Use Promise.all to Stop Async/Await from Blocking Execution in JS
When writing asynchronous code, async/await is a powerful tool — but it comes with risks! Learn how to avoid code slowdowns in this tutorial.
Promises are extremely powerful for handling asynchronous operations in JavaScript. Async functions make them easier to read and reason about. However, they also introduce some sneaky traps that can lead to slowdowns if we’re not careful.
tl;dr
To make sure your async code isn’t slowing down your apps, check your code to ensure that:
- If you’re calling async functions with
await
, don’t let unrelated async calls block each other. - Don’t use
await
inside loops. Create an array of Promises andawait Promise.all
instead.
This article will walk through a few examples and how they can be refactored to avoid blocking execution when using await
.
Promises are a wonderful, powerful tool.
When sending off requests to load third-party data or do other asynchronous work, using a Promise
has become a common pattern for telling our code to wait until the async work is done before continuing.
We can see this in action by writing a function that simulates a slow network connection: when the function is called, it will wait 1 second before resolving (that’s Promise-jargon for “the asynchronous work is complete”).
This is a code-along article!
All the code samples in this article can be run in your console. To see these in action, open up developer tools (command + option + I
), copy paste the code sample into the console, and press enter.
If you don’t want to use your console, you can also use CodePen or create a JavaScript file in your favorite editor to run in your browser.
Promises power many of our data fetching workflows.
One of the most common ways we work with Promises is when loading data with the Fetch API or a library like Axios. For example, if we want to load a random dog image from Dog CEO, our code might use the Fetch API (and Promises) like this:
Fetch sends off a request to the Dog CEO REST API and waits for a response. Once the response comes back, it resolves a Promise with the data that came back. It provides helpers for handling different response types (JSON in this case), which are called in a .then
, and after that point we can do whatever we want with the data by chaining additional .then
calls.
Promises don’t return values for use outside themselves.
When I started learning Promises, I struggled to wrap my head around the idea that there’s no straightforward way to get the data out of a Promise; you have to work inside the .then
once you’ve started using them:
The reasoning makes sense: Promises are asynchronous, but they shouldn’t block synchronous code that doesn’t depend on them, so the asynchronous code has to be isolated.
However, trying to “think in Promises” has caused me a lot of headaches. It always takes me a minute to get my brain into the right mode to write Promise-based code.
Nested Promises are hard to keep track of.
If our content has multiple async steps, the nesting in Promises can become challenging to keep track of:
Because next steps have to happen inside a .then()
, each async action further nests our code, which adds cognitive overhead and can make code hard to refactor and maintain as time goes on.
Async functions make Promises easier to use…
To make Promises easier to work with, async functions introduce the async
and await
keywords that allow us to get the benefits of Promises — waiting for an async all to complete before continuing — without the mental overhead of chaining .then
calls and nesting Promises.
Let‘s refactor the code we’ve written so far in this article using async functions to make it a little easier to read:
The await
tells our code to wait for the Promise to resolve, then hands back the resolved value of the Promise as the return value. This removes a lot of boilerplate associated with Promises, and that’s a Good Thing™.
…but async functions also introduce new challenges.
Unfortunately, the convenience of async functions comes with some traps that aren’t immediately obvious.
Let’s take a look at two examples:
- Two unrelated async operations that load data, then do separate things.
- An async chain: one operation that loads an array, then a series of calls that depend on the result of the original call.
Two unrelated async operations
With Promises, we might set this up like so:
If you run this in your console, you’ll see that Thing 1 and Thing 2 complete at nearly the same time.
If we refactor to use async functions, our code might look like this:
This looks nice and clean, but if we run it in our console we’ll notice an issue: the code now takes twice as long to run! 😱
Promises run in parallel because they don’t care what happens outside of them — that‘s why we can only access their content inside a .then
. This means that both asyncThing1()
and asyncThing2()
run at the same time in our first example.
In async functions, await
blocks any code that follows from executing until the Promise has resolves, which means that our refactored code doesn’t even start asyncThing2()
until asyncThing1()
has completed — that’s not good.
There’s good news, though: we can fix this without giving up on the benefits of async functions!
Because async functions are Promises under the hood, we can run both asyncThing1()
and asyncThing2()
in parallel by calling them without await
. Then we can use await
and Promise.all
, which returns an array of results once all Promises have completed.
This allows the Promises to run in parallel again, but still gives us a pleasant-to-use syntax that avoids chaining and lets us treat the values in our Promises as standard return values.
Async operations with dependencies
In some cases, we’ll have async operations that depend on the results of previous async operations. For example, if we want to load a list of blog posts from one API endpoint, then load comment data for each blog post from another.
With Promises, that setup might look like this:
This code is perfectly fine. However, the nesting is pretty deep here, and it’s a little challenging to track where things are happening — we’ve got callbacks in loops in callbacks.
To clean this up, our first instinct might be to refactor this code to use async functions:
So much cleaner! 😍
Unfortunately, each request for comments now has to wait for the one before it to complete, meaning our code is now significantly slower — and it’ll only get worse as we add more posts!
To clean this up, we can use .map
instead of a for loop and create an array of Promises, then use Promise.all
to wait for them to complete.
This code is still much more readable than the nested Promises and for loops, and it allows the requests to load comments to happen in parallel.
Keep async functions fast!
This is just one optimization to keep in mind when writing asynchronous code. Tuck it into your toolkit and keep your async functions fast!
Have other ideas for speeding up async functions? Hit me up on Twitter!