Promise.any Polyfill
Implement Promise.any: first fulfillment wins, all-reject becomes AggregateError. Opposite of all, not a synonym for race.
- 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.messageparsing.errors[i]is the reason for inputi. Keep index alignment.- Wrap thenables with
Promise.resolve. Empty is reject, not pending.- Opposite of
all(first reject loses there). Not a synonym forrace.
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 on this site
- Promise.race polyfill
- Promise.all polyfill
- Promise.allSettled polyfill
- Promises
- The event loop
- Polyfill questions playbook
- Error types and custom errors
Related guides
- Custom Promise From ScratchBuild a tiny Promise: executor, then chaining, microtask reactions, resolve-thenable unwrapping, and the states you are not allowed to leave.
- Promise.all PolyfillImplement Promise.all the way interviews score it: order-preserving results, fail-fast reject, and thenable wrapping with Promise.resolve.
- Promise.allSettled PolyfillWrite Promise.allSettled as a status bag, not a silent all. Fulfilled and rejected entries, order, and why it never rejects for input failures.
- Promise.finally PolyfillPolyfill Promise.prototype.finally: run cleanup on fulfill and reject, pass the original value through, and wait if the callback returns a thenable.
- Promise.race PolyfillImplement Promise.race: first settlement wins, including a fast rejection. Timeouts, cancellation myths, and the empty-iterable hang.