intermediateCommon4 min read · Updated Jul 18, 2026

Implement Promise.any from Scratch

Implement Promise.any in JavaScript with AggregateError semantics: first success wins, errors collected in input order, and the empty-iterable rejection.


The problem

Implement promiseAny(iterable): fulfill with the first fulfilled promise's value; reject only if every promise rejects, with an AggregateError containing all reasons in input order.

Promise.any (ES2021) is the mirror image of Promise.all: all short-circuits on the first failure; any short-circuits on the first success. Implementations are near-isomorphic — swap the roles of resolve and reject — and interviewers often ask them back-to-back precisely to see if you notice the symmetry. Say it; it reframes the whole question as one you've already solved.

Where you see it in production

First-responder patterns: query multiple redundant endpoints/CDN mirrors and take whoever answers first successfully — a failed mirror shouldn't win the race, which is exactly why Promise.race (first settle, including failures) is the wrong tool. Also: racing cache vs network where a cache miss rejects.

Implementation

function promiseAny(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable);
    const errors = new Array(items.length); // INDEX-addressed, like all's results
    let remaining = items.length;
 
    if (remaining === 0) {
      // No promise can ever fulfill → reject NOW (spec behavior)
      reject(new AggregateError([], "All promises were rejected"));
      return;
    }
 
    items.forEach((item, index) => {
      Promise.resolve(item).then(
        resolve, // first fulfillment wins; settle-once ignores the rest
        (reason) => {
          errors[index] = reason; // input order, NOT rejection order
          remaining -= 1;
          if (remaining === 0) {
            reject(new AggregateError(errors, "All promises were rejected"));
          }
        }
      );
    });
  });
}

Verified behavior

await promiseAny([
  new Promise((_, rej) => setTimeout(rej, 100, "fast failure")),
  new Promise((res)    => setTimeout(res, 200, "slow success")),
]);
// "slow success" — failures don't win, unlike race
 
promiseAny([
  Promise.reject("e1"),
  Promise.reject("e2"),
]).catch((err) => {
  err instanceof AggregateError; // true
  err.errors;                    // ["e1", "e2"] — input order
});
 
promiseAny([]).catch((err) => err instanceof AggregateError); // true — immediate

The two planted bugs

  1. errors.push(reason) — collects reasons in rejection order. The spec (and any consumer that correlates err.errors[i] back to input i) requires input order: errors[index] = reason. Identical to the results.push bug in Promise.all — same fix, same reason.
  2. Empty iterable hangs — with no items, no callback ever fires. Promise.any([]) must reject immediately with an empty AggregateError. Note the asymmetry with all([]) (resolves) — vacuous truth: "all of zero promises fulfilled" is true, "at least one of zero promises fulfilled" is false. That one-sentence logic explanation is an instant senior signal.

Edge cases interviewers probe

  • AggregateError — a real ES2021 error class with an .errors array. Rejecting with an array of reasons, or a generic Error, is observably wrong (err instanceof AggregateError fails).
  • Mixed valuespromiseAny([Promise.reject("x"), 42]) fulfills with 42; plain values count as fulfillments.
  • Multiple fulfillments — first resolve wins; later ones hit a settled promise. No special handling needed — but you should be able to explain why (settle-once).
  • Late rejections after a win — decrements continue harmlessly; reject on a settled promise is a no-op. No unhandled-rejection warnings, because every rejection was handled by your handler.
  • Iterables beyond arrays — same Array.from treatment as the other combinators.

Common mistakes

  • The push-vs-index ordering bug (planted bug #1).
  • Missing the empty-iterable rejection (planted bug #2 — and the empty-case behavior differs across all four combinators, which is why interviewers love it).
  • Confusing any with race — "first success" vs "first settle" — the single most common conceptual slip in this family.
  • new AggregateError(errors) built from a shared array that later rejections keep mutating — harmless here since rejection happens once at the end, but if you build it incrementally, freeze your semantics first.
  • Forgetting Promise.resolve wrapping, so plain values or foreign thenables break.

The combinator family

Resolves whenRejects whenEmpty input
allevery fulfillsfirst rejectionresolves []
allSettledevery settlesneverresolves []
anyfirst fulfillmentall reject (AggregateError)rejects
racefirst settlefirst settlepends forever

Follow-up questions

  • "Implement any in terms of all." — invert every promise (fulfillments become rejections and vice versa), run all, invert the result. Cute, real, and a good test of whether the symmetry actually clicked.
  • "When race and when any?"race when failures should also short-circuit (timeouts!); any when failures should be tolerated while waiting for a success.
  • "How do you cancel the losers?" — you don't, with promises alone; pass an AbortSignal to each contender and abort the rest on first success — the hedged-request pattern used in real RPC stacks.
  • "What does err.errors contain if some promises never settled?" — it can't happen: any only rejects after all have rejected. If some pend forever, any pends forever too — which is why production usage pairs it with timeouts.
  • "Why did this land in ES2021 and not earlier?" — it needed AggregateError; multi-cause errors weren't representable in the standard library before.

  • Promise.race

    beginner

    Implement Promise.race and use it for the timeout pattern every senior interview touches.

  • Promise.allSettled

    intermediate

    Implement allSettled: wait for every promise and report per-promise status objects.

  • Promise.all

    intermediate

    Implement Promise.all: aggregate results in order, fail fast on the first rejection.