Using Promise.any() in Practice: The First Successful Result Wins
AI generated
JS
() =>
JavaScript · Promises · Asynchronous Programming
Using Promise.any() in Practice
The first successful result wins, failures are ignored until the end

When several mirror APIs could return the same answer and only the fastest successful result matters, Promise.any() is the right combinator, clearly distinct from Promise.race() and Promise.allSettled().

13 min read Promise.any() AggregateError Async/Await

1. Four Promise Combinators, Four Scenarios

ES2020 brought Promise.allSettled() and Promise.any() as the last two missing pieces in the quartet of promise combinators. Promise.all() waits for every success and aborts on the first failure, Promise.race() reacts to the first result of any kind, success or failure, Promise.allSettled() consistently waits for every promise and delivers a complete tally.

Promise.any() closes the remaining gap: 'give me the first successful result, ignore individual failures as long as at least one promise succeeds.' That matches exactly the scenario of several redundant data sources, for example several CDN mirrors of the same file, where a single slow or broken server should not delay the overall response.

2. Promise.any(): Basics

Promise.any(iterable) takes an iterable of promises and itself returns a promise that fulfills as soon as the first passed promise fulfills, with exactly that value. All other, still-running promises are not cancelled, their later results are simply ignored, unless you explicitly take care of cancelling them yourself.

A typical example: three mirror servers offer the same configuration file. Instead of querying them sequentially one after another, or blindly picking the first one in the list, you query all three in parallel via fetch() and use Promise.any() to automatically get the fastest successful response, regardless of which server delivers it.


const mirrors = [
  'https://mirror1.example.com/config.json',
  'https://mirror2.example.com/config.json',
  'https://mirror3.example.com/config.json',
];

const response = await Promise.any(mirrors.map((url) => fetch(url)));
const config = await response.json();

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

The key difference: Promise.race() settles on whichever result arrives first in time, regardless of whether it is a success or a failure. If the fastest of three servers fails first, even though the other two would answer successfully shortly after, Promise.race() immediately propagates that failure, the successful responses arrive too late to change anything.

Promise.any(), by contrast, deliberately ignores individual failures and keeps waiting as long as at least one promise is still pending. Only when truly all passed promises fail does Promise.any() also reject. For redundant, fault-tolerant scenarios like mirror servers, that is almost always the desired behavior, while race() fits better for genuine time limits against a single expected source.


// race(): first result wins, even a failure
const raceResult = await Promise.race([
  fetch('https://slow-but-reliable.example.com'),
  fetch('https://fast-but-broken.example.com'), // fails first
]); // throws the error, even though the reliable server would have answered soon

// any(): failures are ignored as long as an alternative exists
const anyResult = await Promise.any([
  fetch('https://fast-but-broken.example.com'),
  fetch('https://slow-but-reliable.example.com'),
]); // returns the reliable server's response

4. Difference From Promise.allSettled()

Promise.allSettled() pursues a different goal than Promise.any(): it consistently waits for every promise, regardless of success or failure, and in the end delivers an array with the status of each individual promise ('fulfilled' or 'rejected') along with its value or reason. There is no shortcut, the last result determines when the combined promise settles.

The use case differs accordingly: allSettled() fits when you need a complete overview of every result, for example for a dashboard displaying the status of several independent health checks. any() fits when only a single successful result matters and the remaining promises become irrelevant to the actual task as soon as one of them succeeds.

5. Handling AggregateError Correctly

If truly every passed promise fails, Promise.any() does not reject with a single error, it rejects with an instance of AggregateError, an error type available since ES2021 that bundles several underlying errors. The .errors property is an array containing the individual rejection reasons in the same order as the original promises in the iterable.

Anyone who just wants to react to a failure in general can treat AggregateError like any other Error in a catch block. Anyone who wants to know specifically why each individual source failed, for example for logging or diagnostics, iterates over error.errors and evaluates each cause separately instead of settling for a generic error message.


try {
  await Promise.any(mirrors.map((url) => fetch(url)));
} catch (error) {
  if (error instanceof AggregateError) {
    console.error('All mirror servers failed:');
    error.errors.forEach((err, i) => console.error(`Server ${i}:`, err.message));
  }
}

6. Practical Example: Fastest API Response, Cancelling the Losers

In production code it is worth not just leaving the 'losing' requests running quietly in the background, but actively cancelling them once a result is known, to save network and server load. That is done by combining Promise.any() with AbortController: each request gets its own abort signal, and as soon as one succeeds, the remaining controllers are aborted manually.

This combination matters especially on mobile clients, where unnecessarily continuing requests cost data volume and battery. The trade-off: the extra code for controller management noticeably increases complexity, so the effort pays off mainly for genuinely expensive or frequently executed redundancy requests, not for every arbitrary any() call.


async function fetchFastest(urls) {
  const controllers = urls.map(() => new AbortController());
  const requests = urls.map((url, i) =>
    fetch(url, { signal: controllers[i].signal })
  );
  const winner = await Promise.any(requests);
  controllers.forEach((c) => c.abort()); // cancel the losers
  return winner;
}

7. Combining With a Timeout

Promise.any() has no built-in timeout by default, it theoretically waits indefinitely as long as at least one promise is still pending. If all sources are slow rather than broken, that can lead to undesirably long waits. The solution is to add a time limit to the any() call itself via Promise.race(), which throws its own error when exceeded.

It is important that the timeout itself is formulated as a rejecting promise, not as another element inside the any() array, otherwise any() would simply ignore the timeout error like any other failure as long as a real source is still pending. So the timeout has to sit at the level above Promise.any(), via race() between the any() result and a delayed reject.


function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

const result = await withTimeout(Promise.any(mirrors.map((u) => fetch(u))), 3000);

8. Edge Cases: Empty Array and a Single Promise

An often overlooked special case: Promise.any([]) with an empty iterable rejects immediately and synchronously with an AggregateError whose .errors array is empty, since logically there can be no first successful promise if no promise exists at all. Code that dynamically builds lists from user input or configuration should catch or explicitly handle this case before the call.

If only a single promise is passed, Promise.any() behaves like a plain await on success, but on failure it diverges: instead of throwing the original error directly, it wraps it in an AggregateError with a single entry in the .errors array, which error handling code meant to generically cover one or multiple sources must account for.


try {
  await Promise.any([]);
} catch (error) {
  console.log(error instanceof AggregateError); // true
  console.log(error.errors); // []
}

9. Best Practices and Summary

Promise.any() is the right combinator whenever several redundant sources can deliver the same result and only the fastest successful response matters, while individual failures should be tolerated. The distinction from race() (which also reacts to failures) and allSettled() (which always waits for everything) is the most important conceptual difference.

Production code should almost always include explicit AggregateError handling for the case where truly every source fails, plus, depending on the scenario, an additional timeout wrapper and actively cancelling losing requests via AbortController to save resources instead of letting them fizzle out in the background.

Combinator Fulfills On Rejects On Typical Use
Promise.all() All succeed First failure Dependent parallel tasks
Promise.race() First result of any kind First failure of any kind Genuine time limits
Promise.any() First success When all fail (AggregateError) Redundant mirror sources
Promise.allSettled() Always (after all) Never Complete status overview

Mironsoft

Modern browser APIs, performance, and maintainable JavaScript

JavaScript that holds up in the real browser, not just in the tutorial?

We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.

Code Review

Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.

Performance Optimization

Improving bundle size, load time, and runtime performance with modern APIs.

Modernization

Deliberately introducing native browser APIs instead of heavy libraries.

10. Summary

Promise.any(): The Essentials at a Glance

Core behavior

Promise.any() fulfills with the first success and ignores individual failures as long as an alternative is still pending.

Distinction from race()

race() also reacts to the first failure, any() consistently waits for a success instead.

AggregateError

If all promises fail, any() rejects with AggregateError, whose .errors array contains every individual failure.

No built-in timeout

any() waits indefinitely, a time limit has to be added via Promise.race() around the any() call.

11. FAQ: Promise.any(): The Essentials at a Glance

1When should I use Promise.any() instead of Promise.race()?
Whenever individual failures should be tolerated and only the first successful result matters, for example with redundant mirror servers. race() fits better for genuine time limits against a single expected source.
2What happens if all promises fail with any()?
The combined promise rejects with an AggregateError, whose .errors array contains every individual rejection reason in the original order.
3Does Promise.any() automatically cancel the remaining promises?
No, all remaining promises keep running unchanged, their results are just ignored. Active cancellation additionally requires an AbortController per request.
4Is there a built-in timeout for Promise.any()?
No, any() theoretically waits indefinitely. A time limit can be added via Promise.race() between the any() call and a delayed reject.
5What does Promise.any([]) return with an empty array?
It rejects immediately and synchronously with an AggregateError whose .errors array is empty, since there is no promise that could succeed.
6How does any() differ from allSettled()?
allSettled() always waits for every promise and delivers a complete status overview, any() instead returns only the first successful result and ignores the rest.
7Is AggregateError a normal Error?
It inherits from Error and can be handled like any other error in try/catch, but additionally offers the .errors array for the individual underlying failures.
8What happens with exactly one passed promise?
On success, any() behaves like a plain await, on failure it diverges: the original error is still wrapped in an AggregateError with one entry, not thrown directly.
9Is Promise.any() suited for sequential fallback chains?
Only partially, since all promises start in parallel. For genuine ordering with fallback only on failure, a sequential await chain or a custom retry mechanism fits better.
10Since when is Promise.any() available?
Since ES2021, supported in all current evergreen browsers and in Node.js from version 15 onward, older environments need a polyfill or core-js.