Portrait Dr. Axel Rauschmayer
Dr. Axel Rauschmayer
Homepage | Twitter
Cover of book “JavaScript for impatient programmers”
Book, exercises, quizzes
(free to read online)
Cover of book “Deep JavaScript”
Book (50% free online)
Logo of newsletter “ES.next news”
Newsletter (free)

ECMAScript proposal: Promise.any()

[2019-12-20] dev, javascript, es proposal
(Ad, please don’t block)

The ECMAScript proposal “Promise.any()” (by Mathias Bynens, Kevin Gibbons, and Sergey Rubanov) introduces a new Promise combinator for JavaScript. This blog post explains how it works.

Background information  

The following two sources contain background information that may be useful for understanding this blog post:

Promise.any()  

This is the type signature of Promise.any():

Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>

Promise.any() returns a Promise p. How it is settled, depends on the parameter promises (which refers to an iterable over Promises):

  • If and when the first Promise is fulfilled, p is resolved with that Promise.
  • If all Promises are rejected, p is rejected with an instance of AggregateError that contains all rejection values.

This is the type signature of AggregateError (a few members were omitted):

class AggregateError {
  constructor(errors: Iterable<any>, message: string);
  get errors(): Array<any>;
  get message(): string;
}

The following diagram illustrates how Promise.any() works:

This is what happens if one Promise is fulfilled:

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.resolve('result'),
];
Promise.any(promises)
  .then((result) => assert.equal(
    result, 'result'
  ));

This is what happens if all Promises are rejected:

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.reject('ERROR C'),
];
Promise.any(promises)
  .catch((aggregateError) => assert.deepEqual(
    aggregateError.errors,
    ['ERROR A', 'ERROR B', 'ERROR C']
  ));

Promise.any() vs. Promise.all()  

There are two ways in which Promise.any() and Promise.all() can be compared:

  • They are inverses of each other:
    • Promise.all(): First input rejection rejects the result Promise or its fulfillment value is an Array with input fulfillment values.
    • Promise.any(): First input fulfillment fulfills the result Promise or its rejection value is an Array with input rejection values.
  • The have different focuses:
    • Promise.all() is interested in all fulfillments. The opposite case (at least one rejection) leads to a rejection.
    • Promise.any() is interested in the first fulfillment. The opposite case (only rejections) leads to a rejection.

Promise.any() vs. Promise.race()  

Promise.any() and Promise.race() are also related, but interested in different things:

  • Promise.race() is interested in settlements. The Promise which is settled first, “wins”. In other words: We want to know about the asynchronous computation that terminates first.
  • Promise.any() is interested in fulfillments. The Promise which is fulfilled first, “wins”. In other words: We want to know about the asynchronous computation that succeeds first.

The main – relatively rare – use case for .race() is timing out Promises. The use cases for .any() are broader. We’ll look at them next.

Use cases for Promise.any()  

We use Promise.any() if we have multiple asynchronous computations and we are only interested in the first successful one. In a way, we let the computations compete with each other and use whichever one is fastest.

The following code demonstrates what that looks like when downloading resources:

const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

We can use the same pattern to use whichever module downloads more quickly:

const lodash = await Promise.any([
  import('https://primary.example.com/lodash'),
  import('https://secondary.example.com/lodash'),
]);

For comparison, this is the code we’d use if the secondary server is only a fallback – in case the primary server fails:

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}

How would we implement Promise.any()?  

A simple implementation of Promise.any() is basically a mirror version of an implementation of Promise.all(). Check out the code in the blog post on Promise combinators for more information.

Support for Promise.any()