Asynchronous JavaScript 101
Charlotte Fereday
Posted on November 23, 2020
This blog post was originally published in Tes Engineering blog.
Here is a short recap of some fundamentals of using asynchronous JavaScript with some practical examples.
Why do I need to use asynchronous code again?
JavaScript by its nature is synchronous. Each line is executed in the order it appears in the code. It’s also single threaded, it can only execute one command at a time.
If we have an operation that takes some time to complete, we are effectively blocked waiting for it. A couple of common scenarios where this could happen are calling an API and waiting for a response, or querying a database and waiting for the results. Ultimately the impact of this is a slow and frustrating user experience, which can lead to users dropping off your website.
Asynchronous programming offers a way to bypass the synchronous single threaded nature of JavaScript, enabling us to execute code in the background.
Promises
Promises enable asynchronous programming in JavaScript. A promise creates a substitute for the awaited value of the asynchronous task and lets asynchronous methods return values like synchronous methods. Instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some future point.
Let's look at a couple of common ways of implementing Promises. The sample code is extracted from a toy project Security Dashboard I’m working on, more here for the curious.
Chained Promises
const fetchLatestDevToNewsPromiseChaining = () => {
return fetch('https://dev.to/api/articles?per_page=5&tag=security')
.then(response => response.json())
.then(latestArticles => keyDevToInfo(latestArticles))
.catch(err)
};
JavaScript’s built in Fetch API returns a promise object which we can then ‘chain’ promise methods on to, in order to handle the response.
.then()
passes the return value of its callback to the function in the subsequent .then()
, whilst .catch()
handles a rejected promise. We can keep ‘chaining’ on more handling of the results by adding more promise methods.
Async / await
const fetchLatestDevToNewsAsyncAwait = async () => {
try {
const response = await fetch("https://dev.to/api/articles?per_page=5&tag=security")
const latestArticles = await response.json()
return keyDevToInfo(latestArticles)
} catch (err) {
return err
}
}
The other common approach is to use async / await. We use the keyword async
on the function declaration and then await
immediately before the request to the API. Rather than using the promise methods to handle the response, we can simply write any further handling in the same way as any other synchronous JavaScript.
As we’re not using promise methods here we should handle any rejected promises using a try / catch block.
What you’ll notice in both cases is that we don’t need to literally create the Promise object: most libraries that assist with making a request to an API will by default return a promise object. It’s fairly rare to need to use the Promise constructor.
Handling promises
Whether you’re using chained promises or async / await to write asynchronous JavaScript, a promise will be returned, and so when calling the function wrapping the asynchronous code we also need to settle the promise to get the value back.
There are some ways these can be handled via built in iterable methods from JavaScript, here are a few very handy ones for settling results of multiple promises:
Promise.all
Promise.all([fetchLatestDevToNewsPromiseChaining(), fetchLatestDevToNewsAsyncAwait()])
.then(([chained, async]) => {
createFile([...chained, ...async])
})
Promise.all is a good option for asynchronous tasks that are dependent on another. If one of the promises is rejected, it will immediately return its value. If all the promises are resolved you’ll get back the value of the settled promise in the same order the promises were executed.
This may not be a great choice if you don’t know the size of the array of promises being passed in, as it can cause concurrency problems.
Promise.allSettled
Promise.allSettled([fetchLatestDevToNewsPromiseChaining(), fetchLatestDevToNewsAsyncAwait()])
.then(([chained, async]) => {
createFile([...chained, ...async])
})
Promise.allSettled is handy for asynchronous tasks that aren’t dependent on one another and so don’t need to be rejected immediately. It’s very similar to Promise.all except that at the end you’ll get the results of the promises regardless of whether they are rejected or resolved.
Promise.race
Promise.race([fetchLatestDevToNewsPromiseChaining(), fetchLatestDevToNewsAsyncAwait()])
.then(([chained, async]) => {
createFile([...chained, ...async])
})
Promise.race is useful when you want to get the result of the first promise to either resolve or reject. As soon as it has one it will return that result - so it wouldn’t be a good candidate to use in this code.
So ... should I use chained promises or async / await?
We’ve looked at two common approaches for handling asynchronous code in JavaScript: chained promises and async / await.
What’s the difference between these two approaches? Not much: choosing one or the other is more of a stylistic preference.
Using async / await makes the code more readable and easier to reason about because it reads more like synchronous code. Likewise, if there are many subsequent actions to perform, using multiple chained promises in the code may be harder to understand.
On the other hand, it could also be argued that if it’s a simple operation with few subsequent actions chained then the built in .catch()
method reads very clearly.
Whichever approach you take, thank your lucky stars that you have the option to avoid callback hell!
Posted on November 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 16, 2024