Running Promises In Parallel: A Visual Guide

A visual guide to dealing with multiple promises efficiently using the Promise concurrency methods.

Published

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:

PendingFulfilledRejected

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

  1. 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.
  2. 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.

async function fetchPortData() {
const vessels = await fetch("api/vessels");
const tugs = await fetch("api/tugs");
const docks = await fetch("api/docks");
const berths = await fetch("api/berths");
const tariffs = await fetch("api/tariffs");

return { vessels, tugs, berths, docks, tariffs };
}
PendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejected
I know there’s more to fetching than shown here, like error handling and checking 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:

  1. Failing fast vs not failing fast. Failing fast here means rejecting when one of the promises in the iterable rejects.
  2. 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 allReturns 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 . It fails fast.

PendingFulfilledRejectedPromise.all([])PendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejected

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 . This implies that it is impossible to make Promise.allSettled return a rejected Promise, it always returns a fulfilled Promise when done.

PendingFulfilledRejectedPromise.allSettled([])PendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejected

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.

PendingFulfilledRejectedPromise.race([])PendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejected

The canonical legit use case of Promise.race is enforcing a timeout on a long-running asynchronous task . 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.

PendingFulfilledRejectedPromise.any([])PendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejectedPendingFulfilledRejected

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 fulfillSome rejectAll 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.

  1. The 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.
    MDN: Promise.all()
  2. 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()
  3. The next bit we need is Promise.race, the little brother of the famous Promise.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!
    Thoughspile.tech — How to timeout a promise
  4. Promise.race(iterable) takes an iterable over Promises (thenables and other values are converted to Promises via Promise.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 by race() is never settled. As an example, let’s use Promise.race() to implement a timeout.
    Exploring JS — 25.11.4 Timing out via Promise.race()
  5. The 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. Unlike Promise.any(), where you specifically want one of the promises to succeed and only care if all promises fail, with Promise.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 use Promise.race().
    Understanding JavaScript Promises Chapter 3: Working with Multiple Promises - The Promise.race() Method - When to Use Promise.race()