Promise combinators

Published · Tagged with ECMAScript ES2020 ES2021

Since the introduction of promises in ES2015, JavaScript has supported exactly two promise combinators: the static methods Promise.all and Promise.race.

Two new proposals are currently making their way through the standardization process: Promise.allSettled, and Promise.any. With those additions, there’ll be a total of four promise combinators in JavaScript, each enabling different use cases.

Here’s an overview of the four combinators:

namedescriptionstatus
Promise.allSettleddoes not short-circuitadded in ES2020 ✅
Promise.allshort-circuits when an input value is rejectedadded in ES2015 ✅
Promise.raceshort-circuits when an input value is settledadded in ES2015 ✅
Promise.anyshort-circuits when an input value is fulfilledadded in ES2021 ✅

Let’s take a look at an example use case for each combinator.

Promise.all #

  • Chrome: supported since version 32
  • Firefox: supported since version 29
  • Safari: supported since version 8
  • Node.js: supported since version 0.12

Promise.all lets you know when either all input promises have fulfilled or when one of them rejects.

Imagine the user clicks a button and you want to load some stylesheets so you can render a completely new UI. This program kicks off an HTTP request for each stylesheet in parallel:

const promises = [
fetch('/component-a.css'),
fetch('/component-b.css'),
fetch('/component-c.css'),
];
try {
const styleResponses = await Promise.all(promises);
enableStyles(styleResponses);
renderNewUi();
} catch (reason) {
displayError(reason);
}

You only want to start rendering the new UI once all requests succeeded. If something goes wrong, you want to instead display an error message as soon as possible, without waiting for other any other work to finish.

In such a case, you could use Promise.all: you want to know when all promises are fulfilled, or as soon as one of them rejects.

Promise.race #

  • Chrome: supported since version 32
  • Firefox: supported since version 29
  • Safari: supported since version 8
  • Node.js: supported since version 0.12

Promise.race is useful if you want to run multiple promises, and either…

  1. do something with the first successful result that comes in (in case one of the promises fulfills), or
  2. do something as soon as one of the promises rejects.

That is, if one of the promises rejects, you want to preserve that rejection to treat the error case separately. The following example does exactly that:

try {
const result = await Promise.race([
performHeavyComputation(),
rejectAfterTimeout(2000),
]);
renderResult(result);
} catch (error) {
renderError(error);
}

We kick off a computationally expensive task that might take a long time, but we race it against a promise that rejects after 2 seconds. Depending on the first promise to fulfill or reject, we either render the computed result, or the error message, in two separate code paths.

Promise.allSettled #

Promise.allSettled gives you a signal when all the input promises are settled, which means they’re either fulfilled or rejected. This is useful in cases where you don’t care about the state of the promise, you just want to know when the work is done, regardless of whether it was successful.

For example, you can kick off a series of independent API calls and use Promise.allSettled to make sure they’re all completed before doing something else, like removing a loading spinner:

const promises = [
fetch('/api-call-1'),
fetch('/api-call-2'),
fetch('/api-call-3'),
];
// Imagine some of these requests fail, and some succeed.

await Promise.allSettled(promises);
// All API calls have finished (either failed or succeeded).
removeLoadingIndicator();

Promise.any #

Promise.any gives you a signal as soon as one of the promises fulfills. This is similar to Promise.race, except any doesn’t reject early when one of the promises rejects.

const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
// Any of the promises was fulfilled.
console.log(first);
// → e.g. 'b'
} catch (error) {
// All of the promises were rejected.
console.assert(error instanceof AggregateError);
// Log the rejection values:
console.log(error.errors);
// → [
// <TypeError: Failed to fetch /endpoint-a>,
// <TypeError: Failed to fetch /endpoint-b>,
// <TypeError: Failed to fetch /endpoint-c>
// ]
}

This code example checks which endpoint responds the fastest, and then logs it. Only if all of the requests fail do we end up in the catch block, where we can then handle the errors.

Promise.any rejections can represent multiple errors at once. To support this at the language-level, a new error type called AggregateError is introduced. In addition to its basic usage in the above example, AggregateError objects can also be programmatically constructed, just like the other error types:

const aggregateError = new AggregateError([errorA, errorB, errorC], 'Stuff went wrong!');