Promise.race
beginnerImplement Promise.race and use it for the timeout pattern every senior interview touches.
Implement Promise.any in JavaScript with AggregateError semantics: first success wins, errors collected in input order, and the empty-iterable rejection.
Implement
promiseAny(iterable): fulfill with the first fulfilled promise's value; reject only if every promise rejects, with anAggregateErrorcontaining 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.
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.
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"));
}
}
);
});
});
}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 — immediateerrors.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.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.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).promiseAny([Promise.reject("x"), 42]) fulfills with 42; plain values count as fulfillments.resolve wins; later ones hit a settled promise. No special handling needed — but you should be able to explain why (settle-once).reject on a settled promise is a no-op. No unhandled-rejection warnings, because every rejection was handled by your handler.Array.from treatment as the other combinators.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.Promise.resolve wrapping, so plain values or foreign thenables break.| Resolves when | Rejects when | Empty input | |
|---|---|---|---|
all | every fulfills | first rejection | resolves [] |
allSettled | every settles | never | resolves [] |
any | first fulfillment | all reject (AggregateError) | rejects |
race | first settle | first settle | pends forever |
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.race when failures should also short-circuit (timeouts!); any when failures should be tolerated while waiting for a success.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.AggregateError; multi-cause errors weren't representable in the standard library before.Implement Promise.race and use it for the timeout pattern every senior interview touches.
Implement allSettled: wait for every promise and report per-promise status objects.
Implement finally correctly: pass values through, and don't swallow rejections.
Implement Promise.all: aggregate results in order, fail fast on the first rejection.