Timers
When writing JavaScript code, you might want to delay the execution of a function.
This is the job of setTimeout
. You specify a callback function to execute later, and a value expressing how later you want it to run, in milliseconds:
setTimeout(() => {
// runs after 2 seconds
}, 2000)
setTimeout(() => {
// runs after 50 milliseconds
}, 50)
This syntax defines a new function. You can call whatever other function you want in there, or you can pass an existing function name, and a set of parameters:
const myFunction = (firstParam, secondParam) => {
// do something
}
// runs after 2 seconds
setTimeout(myFunction, 2000, firstParam, secondParam)
setTimeout
returns the timer id. This is generally not used, but you can store this id, and clear it if you want to delete this scheduled function execution:
const id = setTimeout(() => {
// should run after 2 seconds
}, 2000)
// I changed my mind
clearTimeout(id)
Zero delay
If you specify the timeout delay to 0
, the callback function will be executed as soon as possible, but after the current function execution:
setTimeout(() => {
console.log('after ')
}, 0)
console.log(' before ')
will print before after
.
This is especially useful to avoid blocking the CPU on intensive tasks and let other functions be executed while performing a heavy calculation, by queuing functions in the scheduler.
Some browsers (IE and Edge) implement a
setImmediate()
method that does this same exact functionality, but it’s not standard and unavailable on other browsers. But it’s a standard function in Node.js.
setInterval()
setInterval
is a function similar to setTimeout
, with a difference: instead of running the callback function once, it will run it forever, at the specific time interval you specify (in milliseconds):
setInterval(() => {
// runs every 2 seconds
}, 2000)
The function above runs every 2 seconds unless you tell it to stop, using clearInterval
, passing it the interval id that setInterval
returned:
const id = setInterval(() => {
// runs every 2 seconds
}, 2000)
clearInterval(id)
It’s common to call clearInterval
inside the setInterval callback function, to let it auto-determine if it should run again or stop. For example, this code runs something unless App.somethingIWait has the value arrived
:
const interval = setInterval(() => {
if (App.somethingIWait === 'arrived') {
clearInterval(interval)
return
}
// otherwise do things
}, 100)
Recursive setTimeout
setInterval
starts a function every n milliseconds, without any consideration about when a function finished its execution.
If a function takes always the same amount of time, it’s all fine:
Maybe the function takes different execution times, depending on network conditions for example:
And maybe one long execution overlaps the next one:
To avoid this, you can schedule a recursive setTimeout to be called when the callback function finishes:
const myFunction = () => {
// do something
setTimeout(myFunction, 1000)
}
setTimeout(
myFunction()
}, 1000)
to achieve this scenario:
setTimeout
and setInterval
are available in Node.js, through the Timers module.
Node.js also provides setImmediate()
, which is equivalent to using setTimeout(() => {}, 0)
, mostly used to work with the Node.js Event Loop.
Callbacks
Asynchronicity in Programming Languages
Computers are asynchronous by design.
Asynchronous means that things can happen independently of the main program flow.
In the current consumer computers, every program runs for a specific time slot, and then it stops its execution to let another program continue its execution. This thing runs in a cycle so fast that’s impossible to notice, and we think our computers run many programs simultaneously, but this is an illusion (except on multiprocessor machines).
Programs internally use interrupts , a signal that’s emitted to the processor to gain the attention of the system.
I won’t go into the internals of this, but just keep in mind that it’s normal for programs to be asynchronous, and halt their execution until they need attention, and the computer can execute other things in the meantime. When a program is waiting for a response from the network, it cannot halt the processor until the request finishes.
Normally, programming languages are synchronous, and some provide a way to manage asynchronicity, in the language or through libraries. C, Java, C#, PHP, Go, Ruby, Swift, Python, they are all synchronous by default. Some of them handle async by using threads, spawning a new process.
JavaScript
JavaScript is synchronous by default and is single threaded. This means that code cannot create new threads and run in parallel.
Lines of code are executed in series, one after another, for example:
const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()
But JavaScript was born inside the browser, its main job, in the beginning, was to respond to user actions, like onClick
, onMouseOver
, onChange
, onSubmit
and so on. How could it do this with a synchronous programming model?
The answer was in its environment. The browser provides a way to do it by providing a set of APIs that can handle this kind of functionality.
More recently, Node.js introduced a non-blocking I/O environment to extend this concept to file access, network calls and so on.
Callbacks
You can’t know when a user is going to click a button, so what you do is, you define an event handler for the click event . This event handler accepts a function, which will be called when the event is triggered:
document.getElementById('button').addEventListener('click', () => {
//item clicked
})
This is the so-called callback .
A callback is a simple function that’s passed as a value to another function, and will only be executed when the event happens. We can do this because JavaScript has first-class functions, which can be assigned to variables and passed around to other functions (called higher-order functions )
It’s common to wrap all your client code in a load
event listener on the window
object, which runs the callback function only when the page is ready:
window.addEventListener('load', () => {
//window loaded
//do what you want
})
Callbacks are used everywhere, not just in DOM events.
One common example is by using timers:
setTimeout(() => {
// runs after 2 seconds
}, 2000)
XHR requests also accept a callback, in this example by assigning a function to a property that will be called when a particular event occurs (in this case, the state of the request changes):
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
}
}
xhr.open('GET', 'https://yoursite.com')
xhr.send()
Handling errors in callbacks
How do you handle errors with callbacks? One very common strategy is to use what Node.js adopted: the first parameter in any callback function is the error object: error-first callbacks
If there is no error, the object is null
. If there is an error, it contains some description of the error and other information.
fs.readFile('/file.json', (err, data) => {
if (err !== null) {
//handle error
console.log(err)
return
}
//no errors, process data
console.log(data)
})
The problem with callbacks
Callbacks are great for simple cases!
However every callback adds a level of nesting, and when you have lots of callbacks, the code starts to be complicated very quickly:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
//your code here
})
}, 2000)
})
})
This is just a simple 4-levels code, but I’ve seen much more levels of nesting and it’s not fun.
How do we solve this?
Alternatives to callbacks
Starting with ES6, JavaScript introduced several features that help us with asynchronous code that do not involve using callbacks: Promises (ES2015) and Async/Await (ES2017)
Promises
A promise is commonly defined as a proxy for a value that will eventually become available .
Promises are one way to deal with asynchronous code, without writing too many callbacks in your code.
Although they’ve been around for years, they were standardized and introduced in ES2015, and now they have been superseded in ES2017 by async functions.
Async functions , which we’ll see in the next lesson, use the promises API as their building block, so understanding them is fundamental even if in newer code you’ll likely use async functions instead of promises.
How promises work, in brief
Once a promise has been called, it will start in pending state . This means that the caller function continues the execution, while it waits for the promise to do its own processing, and give the caller function some feedback.
At this point, the caller function waits for it to either return the promise in a resolved state , or in a rejected state , but the function continues its execution while the promise does it work.
Which JS API use promises?
In addition to your own code and library code, promises are used by standard modern Web APIs such as:
- the Battery API
- the Fetch API
- Service Workers
It’s unlikely that in modern JavaScript you’ll find yourself not using promises, so let’s start diving right into them.
Creating a promise
The Promise API exposes a Promise constructor, which you initialize using new Promise()
:
let done = true
const isItDoneYet = new Promise((resolve, reject) => {
if (done) {
const workDone = 'Here is the thing I built'
resolve(workDone)
} else {
const why = 'Still working on something else'
reject(why)
}
})
As you can see the promise checks the done
global constant, and if that’s true, we return a resolved promise, otherwise a rejected promise.
Using resolve
and reject
we can communicate back a value, in the above case we just return a string, but it could be an object as well.
Consuming a promise
In the last section, we introduced how a promise is created.
Now let’s see how the promise can be consumed or used.
const isItDoneYet = new Promise()
//...
const checkIfItsDone = () => {
isItDoneYet
.then(ok => {
console.log(ok)
})
.catch(err => {
console.error(err)
})
}
Running checkIfItsDone()
will execute the isItDoneYet()
promise and will wait for it to resolve, using the then
callback, and if there is an error, it will handle it in the catch
callback.
Chaining promises
A promise can be returned to another promise, creating a chain of promises.
A great example of chaining promises is given by the Fetch API, a layer on top of the XMLHttpRequest API, which we can use to get a resource and queue a chain of promises to execute when the resource is fetched.
The Fetch API is a promise-based mechanism, and calling fetch()
is equivalent to defining our own promise using new Promise()
.
Example of chaining promises
const status = response => {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
}
return Promise.reject(new Error(response.statusText))
}
const json = response => response.json()
fetch('/todos.json')
.then(status)
.then(json)
.then(data => {
console.log('Request succeeded with JSON response', data)
})
.catch(error => {
console.log('Request failed', error)
})
In this example, we call fetch()
to get a list of TODO items from the todos.json
file found in the domain root, and we create a chain of promises.
Running fetch()
returns a response, which has many properties, and within those we reference:
-
status
, a numeric value representing the HTTP status code -
statusText
, a status message, which isOK
if the request succeeded
response
also has a json()
method, which returns a promise that will resolve with the content of the body processed and transformed into JSON.
So given those premises, this is what happens: the first promise in the chain is a function that we defined, called status()
, that checks the response status and if it’s not a success response (between 200 and 299), it rejects the promise.
This operation will cause the promise chain to skip all the chained promises listed and will skip directly to the catch()
statement at the bottom, logging the Request failed
text along with the error message.
If that succeeds instead, it calls the json() function we defined. Since the previous promise, when successful, returned the response
object, we get it as an input to the second promise.
In this case, we return the data JSON processed, so the third promise receives the JSON directly:
.then((data) => {
console.log('Request succeeded with JSON response', data)
})
and we log it to the console.
Handling errors
In the above example, in the previous section, we had a catch
that was appended to the chain of promises.
When anything in the chain of promises fails and raises an error or rejects the promise, the control goes to the nearest catch()
statement down the chain.
new Promise((resolve, reject) => {
throw new Error('Error')
}).catch(err => {
console.error(err)
})
// or
new Promise((resolve, reject) => {
reject('Error')
}).catch(err => {
console.error(err)
})
Cascading errors
If inside the catch()
you raise an error, you can append a second catch()
to handle it, and so on.
new Promise((resolve, reject) => {
throw new Error('Error')
})
.catch(err => {
throw new Error('Error')
})
.catch(err => {
console.error(err)
})
Orchestrating promises
Promise.all()
If you need to synchronize different promises, Promise.all()
helps you define a list of promises, and execute something when they are all resolved.
Example:
const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2])
.then(res => {
console.log('Array of results', res)
})
.catch(err => {
console.error(err)
})
The ES2015 destructuring assignment syntax allows you to also do
Promise.all([f1, f2]).then(([res1, res2]) => {
console.log('Results', res1, res2)
})
You are not limited to using fetch
of course, any promise is good to go .
Promise.race()
Promise.race()
runs as soon as one of the promises you pass to it resolves, and it runs the attached callback just once with the result of the first promise resolved.
Example:
const promiseOne = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one')
})
const promiseTwo = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two')
})
Promise.race([promiseOne, promiseTwo]).then(result => {
console.log(result) // 'two'
})
Common errors
Uncaught TypeError: undefined is not a promise
If you get the Uncaught TypeError: undefined is not a promise
error in the console, make sure you use new Promise()
instead of just Promise()
Promise.prototype.finally()
When a promise is fulfilled, successfully it calls the then()
methods, one after another.
If something fails during this, the then()
methods are jumped and the catch()
method is executed.
finally()
allow you to run some code regardless of the successful or not successful execution of the promise:
fetch('file.json')
.then(data => data.json())
.catch(error => console.error(error))
.finally(() => console.log('finished'))
Async/Await
JavaScript evolved in a very short time from callbacks to promises (ES2015), and since ES2017 asynchronous JavaScript is even simpler with the async/await syntax.
Async functions are a combination of promises and generators, and basically, they are a higher level abstraction over promises. Let me repeat: async/await is built on promises .
Why were async/await introduced?
They reduce the boilerplate around promises, and the “don’t break the chain” limitation of chaining promises.
When Promises were introduced in ES2015, they were meant to solve a problem with asynchronous code, and they did, but over the 2 years that separated ES2015 and ES2017, it was clear that promises could not be the final solution .
Promises were introduced to solve the famous callback hell problem, but they introduced complexity on their own, and syntax complexity.
They were good primitives around which a better syntax could be exposed to developers, so when the time was right we got async functions .
They make the code look like it’s synchronous, but it’s asynchronous and non-blocking behind the scenes.
How it works
An async function returns a promise, like in this example:
const doSomethingAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 3000)
})
}
When you want to call this function you prepend await
, and the calling code will stop until the promise is resolved or rejected . One caveat: the client function must be defined as async
. Here’s an example:
const doSomething = async () => {
console.log(await doSomethingAsync())
}
A quick example
This is a simple example of async/await used to run a function asynchronously:
const doSomethingAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 3000)
})
}
const doSomething = async () => {
console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')
The above code will print the following to the browser console:
Before
After
I did something //after 3s
Promise all the things
Prepending the async
keyword to any function means that the function will return a promise.
Even if it’s not doing so explicitly, it will internally make it return a promise.
This is why this code is valid:
const aFunction = async () => {
return 'test'
}
aFunction().then(alert) // This will alert 'test'
and it’s the same as:
const aFunction = async () => {
return Promise.resolve('test')
}
aFunction().then(alert) // This will alert 'test'
The code is much simpler to read
As you can see in the example above, our code looks very simple. Compare it to code using plain promises, with chaining and callback functions.
And this is a very simple example, the major benefits will arise when the code is much more complex.
For example, here’s how you would get a JSON resource, and parse it, using promises:
const getFirstUserData = () => {
return fetch('/users.json') // get users list
.then(response => response.json()) // parse JSON
.then(users => users[0]) // pick first user
.then(user => fetch(`/users/${user.name}`)) // get user data
.then(userResponse => response.json()) // parse JSON
}
getFirstUserData()
And here is the same functionality provided using await/async:
const getFirstUserData = async () => {
const response = await fetch('/users.json') // get users list
const users = await response.json() // parse JSON
const user = users[0] // pick first user
const userResponse = await fetch(`/users/${user.name}`) // get user data
const userData = await user.json() // parse JSON
return userData
}
getFirstUserData()
Multiple async functions in series
Async functions can be chained very easily, and the syntax is much more readable than with plain promises:
const promiseToDoSomething = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 10000)
})
}
const watchOverSomeoneDoingSomething = async () => {
const something = await promiseToDoSomething()
return something + ' and I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
const something = await watchOverSomeoneDoingSomething()
return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
console.log(res)
})
Will print:
I did something and I watched and I watched as well
Easier debugging
Debugging promises is hard because the debugger will not step over asynchronous code.
Async/await makes this very easy because to the compiler it’s just like synchronous code.
Asynchronous iteration
The for-await-of
loop allows you to use an async iterable object as the loop iteration:
for await (const line of readLines(filePath)) {
console.log(line)
}
Since this uses await
, you can use it only inside async
functions, like a normal await
.
How to use Async and Await with Array.map()
You want to execute an async function inside a map()
call, to perform an operation on every element of the array, and get the results back.
How can you do so?
This is the correct syntax:
const list = [] //...an array filled with values
const functionWithPromise = item => { //a function that returns a promise
return Promise.resolve('ok')
}
const anAsyncFunction = async item => {
return await functionWithPromise(item)
}
const getData = async () => {
return await Promise.all(list.map(item => anAsyncFunction(item)))
}
const data = getData()
console.log(data)
The main thing to notice is the use of Promise.all()
, which resolves when all it’s promises are resolved.
list.map()
returns a list of promises, so in result
we’ll get the value when everything we ran is resolved.
Remember, we must wrap any code that calls await
in an async
function.
How to make your JavaScript functions sleep
Sometimes you want your function to pause execution for a fixed amount of seconds or milliseconds.
In a programming language like C or PHP, you’d call sleep(2)
to make the program halt for 2 seconds. Java has Thread.sleep(2000)
, Python has time.sleep(2)
, Go has time.Sleep(2 * time.Second)
.
JavaScript does not have a native sleep function, but thanks to the introduction of promises (and async/await in ES2018) we can implement such feature in a very nice and readable way, to make your functions sleep:
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
You can now use this with the then
callback:
sleep(500).then(() => {
//do stuff
})
Or use it in an async function:
const doSomething = async () => {
await sleep(2000)
//do stuff
}
doSomething()
Remember that due to how JavaScript works, this does not pause the entire program execution like it might happen in other languages, but instead only your function sleeps.
Quiz
Welcome to the quiz! Try to answer those questions, which cover the topics of this module.
You can also write the question/answer into the Discord chat, to make sure it’s correct - other students or Flavio will check it for you!
- write a practical example of using
setInterval()
, and clearing the interval when something happens - do you think there is a problem in having too many callbacks nested one into another?
- which are the 3 states of a promise?
- what does it mean that a promise has resolved?
- how can we wait until multiple promises are resolved?
- what is the purpose of the
finally()
method of a promise? - what is the requirement we must have to use the
await
keyword in a function? - can you explain how an async function call works?