ESC

Type to search the knowledge base.

Promise.any Polyfill

Implement Promise.any: first fulfillment wins, all-reject becomes AggregateError. Opposite of all, not a synonym for race.

intermediate5 min read
  • javascript
  • promises
  • polyfill
  • async

Promise.any is the combinator people describe when they say “try these mirrors, take the first one that works.” Implementing it as Promise.race fails the first time a mirror 404s faster than another 200s. Promise.any ignores rejections until there are none left to ignore. Then it rejects with AggregateError, and error.errors holds every reason in input order.

Empty iterable does not hang (that is race). It rejects with an AggregateError whose errors array is empty. First fulfillment fulfills with that value and does not wait for the rest.

Standalone myPromiseAny. any is ES2021; AggregateError is the error type the spec names, not a generic Error with a concatenated message.

Key takeaways

  • First fulfillment wins. Rejections are stored, not fatal, until every input has rejected.
  • All-reject (and empty input) reject with AggregateError. Read .errors, not .message parsing.
  • errors[i] is the reason for input i. Keep index alignment.
  • Wrap thenables with Promise.resolve. Empty is reject, not pending.
  • Opposite of all (first reject loses there). Not a synonym for race.

First success, or one error that holds every reason

function myPromiseAny(iterable) {
  return new Promise((resolve, reject) => {
    const items = [...iterable];
    const n = items.length;

    if (n === 0) {
      reject(new AggregateError([], 'All promises were rejected'));
      return;
    }

    const errors = new Array(n);
    let remaining = n;

    items.forEach((item, i) => {
      Promise.resolve(item).then(resolve, (reason) => {
        errors[i] = reason;
        remaining -= 1;
        if (remaining === 0) {
          reject(new AggregateError(errors, 'All promises were rejected'));
        }
      });
    });
  });
}

resolve on the first fulfillment is enough. Later fulfillments call resolve again and are ignored. Later rejections still write errors[i] and decrement; if a success already happened, you will not hit remaining === 0 unless every input rejected, which cannot happen if one fulfilled. If you need to be picky: a fulfillment still leaves remaining untouched in this version, so a mixed set never builds AggregateError. That matches the spec: you only reject when all reject.

Empty list: reject immediately. Do not wait for a then that will never run.

await myPromiseAny([
  Promise.reject(new Error('a')),
  Promise.resolve('ok'),
  Promise.reject(new Error('b')),
]); // 'ok'

try {
  await myPromiseAny([
    Promise.reject('x'),
    Promise.reject('y'),
  ]);
} catch (err) {
  err instanceof AggregateError; // true
  err.errors; // ['x', 'y']
}

myPromiseAny([]); // rejects, errors: []

Thenables that fulfill count as success. Thenables that call the reject callback count as one of the reasons.

AggregateError is an Error. instanceof Error is true. The useful field is .errors. Do not join reasons into a string and throw that; callers cannot recover per-input causes.

How it sits next to the other three

Helper Fulfills when Rejects when
all Every input fulfills First reject
any First fulfill Every input rejected (AggregateError)
race First settle (either way) That first settle was a reject
allSettled Every input settled Not for input failures

“First CDN that returns 200” is any. “First to finish, even if it is a 500” is race. “I need every payload or I cannot render” is all. “Render a skeleton for the ones that failed” is allSettled.

Footguns in a Promise.any polyfill

Empty any rejects. Empty race hangs. Mixing those two empty behaviors is the fastest way to fail a follow-up.

If every input but one has rejected, and that last one never settles, any hangs. AggregateError is only for “all rejected,” not for “we got tired of waiting.” Pair a timeout (or abort) at the call site if a mirror can stall.

Do not push reasons. If input 1 rejects last, errors must still be [reason0, reason1, reason2], not shuffled. errors.length equals the input count, including slots whose reason is a thrown thenable.

If AggregateError is missing in a truly old engine, you would polyfill that too (name, errors array, Error prototype). In a 2026 interview, assume AggregateError exists and use it. Throwing new Error(errors) is wrong.

Success does not cancel the other fetches. Same production note as all and race: abort if you care about the network, not if you only care about the first value.

remaining must start at n and only decrement on reject in this sketch. Decrementing on fulfill as well would fire AggregateError after a success once the rest reject. Do not do that.

Non-iterable: spreading throws inside the executor if you move [...iterable] into new Promise, which rejects. Sync throw from myPromiseAny is a lesser answer; native combinators return a rejected promise.

Interview answer (out loud)

“Promise.any fulfills with the first fulfillment and ignores rejections until every input has rejected. Then it rejects with AggregateError, and error.errors is the reasons in input order. An empty iterable rejects with AggregateError and an empty errors array; it does not hang. I wrap thenables with Promise.resolve. This is the opposite of all, and it is not race, because a fast failure must not beat a slow success.”

Further reading

Related guides