Browse by section

Web Design 日本語

How to Use Promise.any() Correctly

Promise.any() returns the value of the first promise to fulfil, and throws an AggregateError if all of them reject. Standardised in ES2021, it works in every modern browser and in Node.js 15 and later.

Its use case is narrow and clear: failover across mirrors or CDNs. Conversely, it cannot be used for checks that return true or false—a very common misuse, covered below. This article explains the correct usage and how it differs from all, race, and allSettled.

Sponsored

How Promise.any() behaves

It returns the value of the first promise to fulfil and ignores the rest. Rejections do not affect the outcome unless every promise rejects.

const p1 = Promise.reject(new Error('error 1'));
const p2 = new Promise((resolve) => setTimeout(resolve, 100, 'success 2'));
const p3 = new Promise((resolve) => setTimeout(resolve, 500, 'success 3'));

const result = await Promise.any([p1, p2, p3]);
console.log(result); // 'success 2'

p1 fails immediately, but Promise.any() waits and returns p2‘s value after 100ms. p3‘s result is discarded—though, importantly, its work does not stop. More on that below.

When everything fails: AggregateError

If all the promises reject, you get an AggregateError. Its distinguishing feature is that the original rejection reasons are in an errors array.

try {
  await Promise.any([
    Promise.reject(new Error('error 1')),
    Promise.reject(new Error('error 2')),
    Promise.reject(new Error('error 3')),
  ]);
} catch (err) {
  if (err instanceof AggregateError) {
    console.error('All failed');
    err.errors.forEach((e) => console.error(' -', e.message));
  }
}

Passing an empty array also rejects immediately with AggregateError. If you build the array dynamically, forgetting this branch produces a confusing error.

const sources = getSources(); // may come back empty

if (sources.length === 0) {
  throw new Error('No sources available');
}

const data = await Promise.any(sources.map(fetchFrom));

Sponsored

The correct use: failover

The intended scenario is the same data available from several places. You only need one to respond, and you want the fastest one.

async function fetchJson(url, signal) {
  const res = await fetch(url, { signal });
  if (!res.ok) throw new Error(`HTTP ${res.status} (${url})`);
  return res.json();
}

const MIRRORS = [
  'https://cdn1.example.com/data.json',
  'https://cdn2.example.com/data.json',
  'https://origin.example.com/data.json',
];

try {
  const data = await Promise.any(MIRRORS.map((u) => fetchJson(u)));
  render(data);
} catch (err) {
  // AggregateError: every mirror is down
  console.error('All sources failed:', err.errors);
  showMessage('Could not load the data');
}

Turning HTTP errors into exceptions is essential here. Because fetch() fulfils on 404 and 500, skipping the res.ok check means “the 404 response was fastest, so we will use that.”

What not to do: validation checks

Sample code like the following, which appears widely online, does not do what it looks like.

// broken
const validateEmail = (v) => Promise.resolve(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v));
const validatePhone = (v) => Promise.resolve(/^\d{10}$/.test(v));

Promise.any([
  validateEmail('not an email'),
  validatePhone('not a phone number'),
]).then((r) => console.log('at least one passed:', r));

This logs “at least one passed: false”. The reason is simple: Promise.resolve(false) is a promise that succeeded with the value false. Promise.any() looks only at fulfilled versus rejected, never at the value itself.

To use it for checks, failure must be expressed as a rejection.

// correct: reject on failure
const validateEmail = async (v) => {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) throw new Error('not an email');
  return { type: 'email', value: v };
};

const validatePhone = async (v) => {
  if (!/^\d{10}$/.test(v)) throw new Error('not a phone number');
  return { type: 'phone', value: v };
};

try {
  const matched = await Promise.any([validateEmail(input), validatePhone(input)]);
  console.log('format:', matched.type);
} catch (err) {
  console.log('matches no known format');
}

That said, there is no reason to wrap synchronous checks in promises at all. The example above is for illustration; in practice Array.prototype.some() is enough. Promise.any() earns its place only when genuinely slow work runs in parallel.

Sponsored

The losers keep running

An easily missed detail: once Promise.any() settles, the remaining operations continue. Promises have no cancellation mechanism.

For network requests you must stop them explicitly with AbortController.

const controller = new AbortController();

try {
  const data = await Promise.any(
    MIRRORS.map((u) => fetchJson(u, controller.signal))
  );
  render(data);
} finally {
  // once a winner exists, cut off the rest
  controller.abort();
}

Putting it in finally matters: it cleans up on both success and failure. Without it, you run as many full requests as you have mirrors, wasting bandwidth and connections.

all, allSettled, race and any

Method Settles when Returns Use for
Promise.all() All succeed, or one fails Array of values You need everything to render
Promise.allSettled() All settle Array with status Partial failure is acceptable
Promise.race() The first to settle (either way) That value or reason Racing against a timeout
Promise.any() The first to succeed That value Failover, mirrors

The confusion between race() and any() is the one that bites:

const fast = Promise.reject(new Error('fast but failed'));
const slow = new Promise((r) => setTimeout(r, 100, 'slow but succeeded'));

await Promise.race([fast, slow]);  // rejects — the fast one failed
await Promise.any([fast, slow]);   // 'slow but succeeded'

race() decides on speed; any() decides on success. Using race() for failover means the fastest failing server takes down the whole operation.

Support

Environment Version
Chrome / Edge 85
Firefox 79
Safari 14
Node.js 15

It is part of ES2021, and no polyfill is needed any more.

Summary

  • Promise.any() returns the value of the first promise to succeed; all failures produce an AggregateError
  • err.errors holds every rejection reason. An empty array rejects immediately
  • Its real use is mirror and CDN failover
  • Promise.resolve(false) counts as success. For checks, express failure with throw
  • Losing operations keep running. Abort them from finally with AbortController
  • race() decides on speed, any() on success
  • With fetch, always check res.ok (see practical fetch())

“Any one of these will do” turns up more often than you would expect. If you have a stack of nested try blocks doing failover, see whether Promise.any() can flatten it.