Running Promises In Parallel: A Visual Guide
A visual guide to dealing with multiple promises efficiently using the Promise concurrency methods.
Promises! They’re everywhere in modern JavaScript, essential for keeping interactions fluid, UI’s snappy, and code readable.
I’m assuming you’re familiar with promises, so let’s skip a fluffy introduction and just do a quick recap of what states a promise can be in.
A promise can be in one of three states:
Pending: The promise hasn’t fulfilled or rejected yet
When a promise is not pending — so rejected or fulfilled — we call it settled. A promise cannot go back to pending once settled.
async
and await
are companions to Promises that make working with them more friendly.
Adding async
to a function makes it
- Always return a Promise.
- The functions return value becomes the Promise fulfillment value
- Exceptions (thrown values) are turned into rejected Promises. An
async
function never throws. - Possible to use the
await
operator within the function.
await
unpacks a Promise by suspending the function and coming back when the Promise settles. This enables the fulfillment value to be used as a standard return value.
async
& await
are neat because they let’s us write code that looks like regular, line-by-line, linear-in-time code running code we’re used to.
One downside is that is that async
functions sometimes looks so much like regular, linear-in-time code that we forget it’s really not.
Async function run outside of normal execution order.
Perhaps the most common pitfall is awaiting multiple promises sequentially. Thereby, execution is blocked and each following promise is not started until the previous one is settled, which makes our code take longer to complete.
response.ok
and all that but that’s beside the point of this post.
If the promises are independent we might as well not wait for any. By independent, I mean the async task does not depend on the results of previous async tasks. We can create them all at once and set them off all at once, running them in parallel. The Promise object has four to do exactly that.
All of these methods accept an iterable — almost always an array — of promises and return a promise. There are two discerning traits between these methods:
- Failing fast vs not failing fast. Failing fast here means rejecting when one of the promises in the iterable rejects.
- Returning a single fulfillment value vs returning all of the fulfillment values of the promises passed.
And that’s why there are 4 of ’m! With this knowledge, we can place ’m in this handy matrix:
Returns all | Returns single | |
---|---|---|
Fails fast | .all | .race |
Does not fail fast | .allSettled | .any |
Which one to use? Well, that depends on what you want.
Promise.all
returns a promise with an array of fulfillment values when all passed promises fulfill.
As soon as one of the promises passed rejects, Promise.all
reject with this rejection reason 1.
It fails fast.
Here’s the code from before but adapted to run the promises in parallel. Suppose we need all data to render the page, if one request fails we need to display an error.
const endpoints = ["vessels", "tugs", "berths", "docks", "tariffs"];async function fetchPortData() { const fetchCalls = endpoints.map((endpoint) => fetch(`api/${endpoint}`)); const results = Promise.all(fetchCalls); return results;}
Failing fast is not always what you want. Sometimes you want all the results, regardless of whether the promises fulfill or reject.
Promise.allSettled
returns an array of objects that describe the outcome of each promise when all promises have settled 2.
This implies that it is impossible to make Promise.allSettled
return a rejected Promise, it always returns a fulfilled Promise when done.
Same as Promise.all
but this time, suppose that not all data is needed to the display the page.
Suppose that each result is shown in its own panel.
When one fetch
fails, an error message is shown the corresponding panel while the others are displayed as usual.
const endpoints = ["vessels", "tugs", "berths", "docks", "tariffs"];async function fetchPortData() { const fetchCalls = endpoints.map((endpoint) => fetch(`api/${endpoint}`)); const results = Promise.allSettled(fetchCalls); return results;}
Promise.race
returns the first Promise to settle, fulfilled or rejected.
It fails fast just like Promise.all
.
The canonical legit use case of Promise.race
is enforcing a timeout on a long-running asynchronous task 3 4 5.
Race the long-running task against the classic ‘sleep’ Promise to short-circuit it:
const timeout = new Promise((_resolve, reject) => { setTimeout(() => { const duration = 5_000; reject(`Timed out after ${duration / 1_000} sec`); }, 5_000);});const readFromCacheWithTimeout = Promise.race([readFromCache, timeout]);
And this seems to be its only use case. I couldn’t find or think of any others. If you know of another, I’d like to know.
Like .allSettled
is the more tolerant version of .all
, .any
is the more tolerant version of .race
.
It fulfills on the first fulfilled Promise, ignoring rejections.
There is one way .any()
can reject and that is when all passed promises reject.
In this case it will return a Promise rejected with a
special and obscure AggregateError
which has an errors
property listing all rejection values.
The typical use case is running multiple async tasks for the same data and using the first to fulfill. For example, reading some data from multiple possible sources — say a cache and a server — and using the fastest response.
const data = Promise.any([readDataFromCache(), fetchData()]);
Another use case is checking if you get a succesful response at all from multiple calls. For example, if you want to check if some service is still up by receiving a successful responses from either of multiple health endpoints:
const checkServiceOnline = Promise.any([ fetch("https://api.example.com/health"), fetch("https://api-backup.example.com/health"), fetch("https://api-backup-2.example.com/health"),]);
A lil quiz to test if you’ve paid attention or just skimmed. Or maybe you already knew, didn’t read any of the above and just want to feel smart by getting the answers right, well that’s fine too.
Q: After which promise(s) does each of the methods return, for the conditions described in each column? Fill in the table. Think about it for a minute, grab pen and paper and scribble the answers down before revealing the answer.
All fulfill | Some reject | All reject | |
---|---|---|---|
.all | |||
.allSettled | |||
.race | |||
.any |
When dealing with multiple promises, be mindful of opportunities to speed up your code by running promises in parallel with one of the four Promise combinator methods.
From time to time, I myself still mindlessly write await
in loops until ESLint’s no-await-in-loop rule dutifully calls me out.
I recommend you enable it in your linting config if you haven’t yet.
Many thanks to
for proofreading this post. Go follow them on Twitter and visit their websites!
Thanks to Manuel Conde for a correction in the quiz.
- Promise combinators · V8
- Use Promise.all to Stop Async/Await from Blocking Execution in JS by Learn With Jason.
- Understanding JavaScript Promises Chapter 3 is dedicated to ”Working with Multiple Promises” and includes a ”When to Use” section for each method.
- Exploring the JavaScript Promise API methods by Ben Ilegbodu.
- Async functions: making promises friendly by Jake Archibald
-
↩
The
MDN: Promise.all()Promise.all()
static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the input's promises fulfill (including when an empty iterable is passed), with an array of the fulfillment values. It rejects when any of the input's promises rejects, with this first rejection reason. -
↩
Return value: Asynchronously fulfilled, when all promises in the given iterable have settled (either fulfilled or rejected). The fulfillment value is an array of objects, each describing the outcome of one promise in the iterable, in the order of the promises passed, regardless of completion order.
MDN: Promise.allSettled() -
↩
The next bit we need is Promise.race, the little brother of the famous
Thoughspile.tech — How to timeout a promisePromise.all
that is useful for about one real world thing, so no one really cares about it. Quoting MDN,Promise.race()
returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise. That's exactly what we need! -
↩
Exploring JS — 25.11.4 Timing out via Promise.race()Promise.race(iterable)
takes an iterable over Promises (thenables and other values are converted to Promises viaPromise.resolve()
) and returns a Promise. The first of the input Promises that is settled passes its settlement on to the output Promise. If [the] iterable is empty then the Promise returned byrace()
is never settled. As an example, let’s usePromise.race()
to implement a timeout. -
↩
The
Understanding JavaScript Promises Chapter 3: Working with Multiple Promises - The Promise.race() Method - When to Use Promise.race()Promise.race()
method is best used in situations where you want to be able to short-circuit the completion of a number of different promises. UnlikePromise.any()
, where you specifically want one of the promises to succeed and only care if all promises fail, withPromise.race()
you want to know even if one promise fails as long as it fails before any other promise fulfills. Here are some situations where you may want to usePromise.race()
.