Implement your own version of Promise.any. You take an iterable of promises (or plain values), kick them all off in parallel, and return a single promise that resolves with the first fulfilment — or, if every input rejects, rejects with an AggregateError carrying every reason in input order. It is the inverse of Promise.all: one success wins, only all-failures lose.
// Returns a Promise that fulfils with the first input to fulfil,
// or rejects with an AggregateError if every input rejects.
function promiseAny<T>(iterable: Iterable<T | PromiseLike<T>>): Promise<T>;
// First fulfilment wins — even when an earlier-indexed promise rejects first.
const bad = Promise.reject('nope');
const slow = new Promise((r) => setTimeout(() => r('slow-win'), 30));
bad.catch(() => {});
promiseAny([bad, slow]).then(console.log); // 'slow-win'
// All reject → AggregateError with errors[i] matching input[i].
promiseAny([Promise.reject('a'), Promise.reject('b')]).catch((err) => {
console.log(err instanceof AggregateError); // true
console.log(err.message); // 'All promises were rejected'
console.log(err.errors); // ['a', 'b'] — input order, not rejection order
});
// Non-promise values are wrapped and treated as already-fulfilled.
promiseAny([Promise.reject('x'), 42]).then(console.log); // 42
// Empty iterable: there is no fulfilment to wait for, so it rejects immediately.
promiseAny([]).catch((err) => {
console.log(err instanceof AggregateError); // true
console.log(err.errors); // []
});
Promise.any only cares about fulfilments. A faster rejection does NOT decide the result — it just gets recorded in errors and we keep waiting. This is the opposite of Promise.race.errors is indexed by input position. If the 0th input rejects last and the 2nd rejects first, errors[0] still holds the 0th input's reason. Don't .push() rejections as they arrive.'All promises were rejected'. Per spec. Tests check the string.AggregateError whose errors is []. There is nothing to fulfil, so we fail immediately.Promise.resolve so plain values, real promises, and thenables share one code path.undefined still wins. Fulfilment is decided by the promise settling fulfilled, not by truthiness — Promise.resolve(undefined) wins over a later rejection.for...of is the right shape.AggregateError rather than throwing to the caller.Promise — not the input, not a thenable. Tests check instanceof Promise.Promise.any to implement promiseAny. That defeats the exercise. You may use new Promise(...), Promise.resolve, and .then.You'll write a function that fans an iterable of promises out in parallel, waits for the first one to fulfil, and resolves with that value — or, if every input rejects, rejects with an AggregateError carrying every reason in input order.
You have three mirrors for the same file — three CDNs, three replicas, three API regions. You only need one of them to answer; whichever responds first is fine. If they all fail, you want to know every reason at once, not just one of them. That's what Promise.any does: pass an array of promises, get one promise back that fulfils with the first success — or rejects with an AggregateError bundling every failure. Your job is to implement it from scratch using only new Promise, Promise.resolve, and .then.
Promise.any is the mirror image of Promise.all. Where all fails on the first rejection and waits out the rest for results, any succeeds on the first fulfilment and waits out the rest for errors. There are two state machines: a settled flag that latches the moment we fulfil, and a remaining counter that ticks down by one with each rejection. When remaining reaches zero and we never fulfilled, we know every input has failed and it's time to throw an AggregateError.
The crucial detail: Promise.any is not Promise.race. Race resolves or rejects with the first settlement — whichever kind. any waits for the first fulfilment, ignoring rejections as they trickle in (except to record them). A fast rejection in any is a noted casualty; a fast rejection in race is game over.
The instinct is to mirror Promise.all and .push() rejections as they arrive — count to inputs.length and throw an AggregateError:
function naive(iterable) {
return new Promise((resolve, reject) => {
const errors = [];
const inputs = [...iterable];
inputs.forEach((p) => {
Promise.resolve(p).then(resolve, (reason) => {
errors.push(reason);
if (errors.length === inputs.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
}
The count adds up — three inputs, three rejections, throw. But run it on this input:
const slow = new Promise((_, r) => setTimeout(() => r('first-input'), 30));
const mid = new Promise((_, r) => setTimeout(() => r('second-input'), 15));
const fast = new Promise((_, r) => setTimeout(() => r('third-input'), 5));
// expected: errors === ['first-input', 'second-input', 'third-input']
// actual: errors === ['third-input', 'second-input', 'first-input']
errors came out in settle order, not input order. The fastest-rejecting input pushed first; the slowest pushed last. Native Promise.any guarantees errors[i] corresponds to inputs[i]. The fix is to capture each input's index up front and write into errors[slot] directly — which then makes errors.length unreliable as a counter (sparse holes inflate it), so we need a separate remaining counter alongside.
function promiseAny(iterable) {
return new Promise((resolve, reject) => {
const errors = [];
let remaining = 0;
let index = 0;
let settled = false;
// Helper: every input has now rejected. Build the AggregateError with the
// spec-mandated message and reject. AggregateError takes (iterable, message).
const failAll = () => {
settled = true;
reject(new AggregateError(errors, 'All promises were rejected'));
};
// Iterate the input exactly once. for...of works on any iterable —
// arrays, generators, custom iterables. If the iterator itself throws
// synchronously (a generator that throws on the first next(), say),
// the catch below converts it into a rejection, matching native behaviour.
try {
for (const item of iterable) {
// Capture the slot BEFORE incrementing index. By the time this input's
// .then callback fires, `index` will have moved on to later items —
// the closure must remember which slot was ours at registration time.
const slot = index++;
remaining++;
// Promise.resolve handles three cases in one call:
// - a real Promise: returned as-is
// - a thenable (object with .then): wrapped into a real Promise
// - a plain value: wrapped into an already-fulfilled Promise
// This is why a plain `42` or a thenable can "win" the race.
Promise.resolve(item).then(
(value) => {
// First fulfilment wins. The latch prevents a later fulfilment
// from calling resolve a second time — Promise would silently
// ignore it, but it would still run pointless callback work.
if (settled) return;
settled = true;
resolve(value);
},
(reason) => {
// A fulfilment may have already won by the time this runs.
// If so, drop the reason on the floor — we no longer care.
if (settled) return;
// Write into the input-indexed slot, NOT errors.push(reason).
// This is what guarantees errors[i] matches input[i] regardless
// of settle order.
errors[slot] = reason;
remaining--;
// remaining hits zero only when every single input has rejected.
// errors.length would be unreliable here — assigning errors[2]
// before errors[0] and errors[1] exist makes length jump to 3
// over two sparse holes (see the counter-pattern diagram).
if (remaining === 0) failAll();
},
);
}
} catch (err) {
// The iterator's Symbol.iterator or next() threw before we could even
// schedule any promises. Treat it as if everything failed: emit the
// AggregateError so the caller's .catch fires consistently.
if (!settled) {
errors[index] = err;
failAll();
}
return;
}
// Empty iterable: no promises were scheduled, remaining is still 0,
// and no fulfilment latched settled. By spec, this case rejects
// immediately with an AggregateError carrying [] — there is nothing
// to fulfil, so we have already exhausted every option.
if (remaining === 0 && !settled) failAll();
});
}
module.exports = { promiseAny };
Three shifts from the naive version. First, we write into errors[slot] instead of .push() — that fixes the ordering bug, since slot i always holds input i's reason no matter when it settles. Second, we count down a remaining counter because comparing errors.length === inputs.length lies when assignments arrive out of order (a sparse hole at index 0 still counts toward .length). Third, the settled flag latches at the first fulfilment so later settlements — fulfilment or rejection — early-out instead of writing into errors or calling resolve twice.
Trace promiseAny([fail, win]) where fail = Promise.reject('first-input') rejects synchronously and win = new Promise(r => setTimeout(() => r('won!'), 20)) fulfils 20ms later.
Synchronous phase (t = 0). We enter the for...of. Two iterations:
item = fail. slot = 0, index becomes 1, remaining becomes 1. We attach .then callbacks to fail. fail is already rejected, so its rejection callback is queued for the next microtask.item = win. slot = 1, index becomes 2, remaining becomes 2. We attach callbacks to win. win is pending — the callback will fire in 20ms.The for...of exits without throwing. remaining === 2 and settled === false, so the post-loop empty-check is skipped. The executor returns. promiseAny returns a pending Promise.
Microtask after t = 0. fail's rejection callback runs. settled is false. We write errors[0] = 'first-input'. remaining becomes 1. Not zero, so we do NOT call failAll() — there's still one input we haven't heard from, and it might fulfil. We keep waiting.
t = 20ms. win's fulfilment callback runs. settled is still false. We set settled = true and call resolve('won!'). The Promise returned from promiseAny fulfils with 'won!'.
Two things to notice: the recorded errors[0] never reached the caller because the next event was a fulfilment, not another rejection — errors is only surfaced via AggregateError, and we only build one if remaining hits zero. And the function did exactly two Promise.resolve(...).then(...) attachments — O(n) work — regardless of how the promises interleaved.
errors.push(reason) instead of errors[slot] = reason. Push appends in settle order. If your inputs are [slow-reject, fast-reject] and the fast one rejects first, errors comes out reversed relative to input. A caller doing errors[i].retry(input[i]) reads the wrong reason against the wrong input. Always assign into a fixed slot captured at registration time.if (errors.length === inputs.length) instead of a remaining counter. Assigning errors[2] = reason when errors[0] and errors[1] are still empty makes .length jump from 0 to 3. You'll throw the AggregateError early with [undefined, undefined, reason] even though two inputs are still pending. Track count separately.settled guard on rejection. Once a fulfilment has won, a later rejection should be silently dropped — not written into errors, not counted, nothing. If you skip the guard you'll mutate state after the promise has already resolved, which is wasted work at best and a memory leak at worst (errors keeps growing for an aggregate nobody will see).new AggregateError(errors) instead of rejecting. AggregateError is the reason, not the value. reject(new AggregateError(...)) makes the returned promise reject; resolve(new AggregateError(...)) would make it fulfil with an Error object, which a caller's .then would receive as a plain value. Use reject.'All promises were rejected'. If you write 'all promises rejected' or anything else, frameworks and tests that string-match on the message break. Pass it as the second argument to the AggregateError constructor — that's the message slot.new AggregateError([], 'All promises were rejected'). Don't resolve(undefined).Promise.all (the inverse). Same scaffolding (new Promise, iterate, attach .then, slot-indexed array, remaining counter) but opposite polarity: results[slot] = value on fulfilment, reject(reason) immediately on rejection, resolve when remaining hits zero. The shape of the code is identical; only what you do on each branch flips.Promise.allSettled. Never short-circuits — waits for every input regardless. Instead of errors or results, you build a single outcomes[slot] = { status: 'fulfilled', value } or { status: 'rejected', reason } on each settlement. The settled latch goes away entirely, since nothing short-circuits.promiseAny([...inputs, rejectAfter(ms)]) where rejectAfter returns a promise that rejects after ms milliseconds. If any input wins before the timer, you get its value; if the timer wins, the AggregateError includes the timer's rejection alongside every other failure — telling you exactly which mirrors were still in flight when you gave up. A small but powerful pattern for "fastest mirror with a deadline."Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Implement your own version of Promise.any. You take an iterable of promises (or plain values), kick them all off in parallel, and return a single promise that resolves with the first fulfilment — or, if every input rejects, rejects with an AggregateError carrying every reason in input order. It is the inverse of Promise.all: one success wins, only all-failures lose.
// Returns a Promise that fulfils with the first input to fulfil,
// or rejects with an AggregateError if every input rejects.
function promiseAny<T>(iterable: Iterable<T | PromiseLike<T>>): Promise<T>;
// First fulfilment wins — even when an earlier-indexed promise rejects first.
const bad = Promise.reject('nope');
const slow = new Promise((r) => setTimeout(() => r('slow-win'), 30));
bad.catch(() => {});
promiseAny([bad, slow]).then(console.log); // 'slow-win'
// All reject → AggregateError with errors[i] matching input[i].
promiseAny([Promise.reject('a'), Promise.reject('b')]).catch((err) => {
console.log(err instanceof AggregateError); // true
console.log(err.message); // 'All promises were rejected'
console.log(err.errors); // ['a', 'b'] — input order, not rejection order
});
// Non-promise values are wrapped and treated as already-fulfilled.
promiseAny([Promise.reject('x'), 42]).then(console.log); // 42
// Empty iterable: there is no fulfilment to wait for, so it rejects immediately.
promiseAny([]).catch((err) => {
console.log(err instanceof AggregateError); // true
console.log(err.errors); // []
});
Promise.any only cares about fulfilments. A faster rejection does NOT decide the result — it just gets recorded in errors and we keep waiting. This is the opposite of Promise.race.errors is indexed by input position. If the 0th input rejects last and the 2nd rejects first, errors[0] still holds the 0th input's reason. Don't .push() rejections as they arrive.'All promises were rejected'. Per spec. Tests check the string.AggregateError whose errors is []. There is nothing to fulfil, so we fail immediately.Promise.resolve so plain values, real promises, and thenables share one code path.undefined still wins. Fulfilment is decided by the promise settling fulfilled, not by truthiness — Promise.resolve(undefined) wins over a later rejection.for...of is the right shape.AggregateError rather than throwing to the caller.Promise — not the input, not a thenable. Tests check instanceof Promise.Promise.any to implement promiseAny. That defeats the exercise. You may use new Promise(...), Promise.resolve, and .then.You'll write a function that fans an iterable of promises out in parallel, waits for the first one to fulfil, and resolves with that value — or, if every input rejects, rejects with an AggregateError carrying every reason in input order.
You have three mirrors for the same file — three CDNs, three replicas, three API regions. You only need one of them to answer; whichever responds first is fine. If they all fail, you want to know every reason at once, not just one of them. That's what Promise.any does: pass an array of promises, get one promise back that fulfils with the first success — or rejects with an AggregateError bundling every failure. Your job is to implement it from scratch using only new Promise, Promise.resolve, and .then.
Promise.any is the mirror image of Promise.all. Where all fails on the first rejection and waits out the rest for results, any succeeds on the first fulfilment and waits out the rest for errors. There are two state machines: a settled flag that latches the moment we fulfil, and a remaining counter that ticks down by one with each rejection. When remaining reaches zero and we never fulfilled, we know every input has failed and it's time to throw an AggregateError.
The crucial detail: Promise.any is not Promise.race. Race resolves or rejects with the first settlement — whichever kind. any waits for the first fulfilment, ignoring rejections as they trickle in (except to record them). A fast rejection in any is a noted casualty; a fast rejection in race is game over.
The instinct is to mirror Promise.all and .push() rejections as they arrive — count to inputs.length and throw an AggregateError:
function naive(iterable) {
return new Promise((resolve, reject) => {
const errors = [];
const inputs = [...iterable];
inputs.forEach((p) => {
Promise.resolve(p).then(resolve, (reason) => {
errors.push(reason);
if (errors.length === inputs.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
}
The count adds up — three inputs, three rejections, throw. But run it on this input:
const slow = new Promise((_, r) => setTimeout(() => r('first-input'), 30));
const mid = new Promise((_, r) => setTimeout(() => r('second-input'), 15));
const fast = new Promise((_, r) => setTimeout(() => r('third-input'), 5));
// expected: errors === ['first-input', 'second-input', 'third-input']
// actual: errors === ['third-input', 'second-input', 'first-input']
errors came out in settle order, not input order. The fastest-rejecting input pushed first; the slowest pushed last. Native Promise.any guarantees errors[i] corresponds to inputs[i]. The fix is to capture each input's index up front and write into errors[slot] directly — which then makes errors.length unreliable as a counter (sparse holes inflate it), so we need a separate remaining counter alongside.
function promiseAny(iterable) {
return new Promise((resolve, reject) => {
const errors = [];
let remaining = 0;
let index = 0;
let settled = false;
// Helper: every input has now rejected. Build the AggregateError with the
// spec-mandated message and reject. AggregateError takes (iterable, message).
const failAll = () => {
settled = true;
reject(new AggregateError(errors, 'All promises were rejected'));
};
// Iterate the input exactly once. for...of works on any iterable —
// arrays, generators, custom iterables. If the iterator itself throws
// synchronously (a generator that throws on the first next(), say),
// the catch below converts it into a rejection, matching native behaviour.
try {
for (const item of iterable) {
// Capture the slot BEFORE incrementing index. By the time this input's
// .then callback fires, `index` will have moved on to later items —
// the closure must remember which slot was ours at registration time.
const slot = index++;
remaining++;
// Promise.resolve handles three cases in one call:
// - a real Promise: returned as-is
// - a thenable (object with .then): wrapped into a real Promise
// - a plain value: wrapped into an already-fulfilled Promise
// This is why a plain `42` or a thenable can "win" the race.
Promise.resolve(item).then(
(value) => {
// First fulfilment wins. The latch prevents a later fulfilment
// from calling resolve a second time — Promise would silently
// ignore it, but it would still run pointless callback work.
if (settled) return;
settled = true;
resolve(value);
},
(reason) => {
// A fulfilment may have already won by the time this runs.
// If so, drop the reason on the floor — we no longer care.
if (settled) return;
// Write into the input-indexed slot, NOT errors.push(reason).
// This is what guarantees errors[i] matches input[i] regardless
// of settle order.
errors[slot] = reason;
remaining--;
// remaining hits zero only when every single input has rejected.
// errors.length would be unreliable here — assigning errors[2]
// before errors[0] and errors[1] exist makes length jump to 3
// over two sparse holes (see the counter-pattern diagram).
if (remaining === 0) failAll();
},
);
}
} catch (err) {
// The iterator's Symbol.iterator or next() threw before we could even
// schedule any promises. Treat it as if everything failed: emit the
// AggregateError so the caller's .catch fires consistently.
if (!settled) {
errors[index] = err;
failAll();
}
return;
}
// Empty iterable: no promises were scheduled, remaining is still 0,
// and no fulfilment latched settled. By spec, this case rejects
// immediately with an AggregateError carrying [] — there is nothing
// to fulfil, so we have already exhausted every option.
if (remaining === 0 && !settled) failAll();
});
}
module.exports = { promiseAny };
Three shifts from the naive version. First, we write into errors[slot] instead of .push() — that fixes the ordering bug, since slot i always holds input i's reason no matter when it settles. Second, we count down a remaining counter because comparing errors.length === inputs.length lies when assignments arrive out of order (a sparse hole at index 0 still counts toward .length). Third, the settled flag latches at the first fulfilment so later settlements — fulfilment or rejection — early-out instead of writing into errors or calling resolve twice.
Trace promiseAny([fail, win]) where fail = Promise.reject('first-input') rejects synchronously and win = new Promise(r => setTimeout(() => r('won!'), 20)) fulfils 20ms later.
Synchronous phase (t = 0). We enter the for...of. Two iterations:
item = fail. slot = 0, index becomes 1, remaining becomes 1. We attach .then callbacks to fail. fail is already rejected, so its rejection callback is queued for the next microtask.item = win. slot = 1, index becomes 2, remaining becomes 2. We attach callbacks to win. win is pending — the callback will fire in 20ms.The for...of exits without throwing. remaining === 2 and settled === false, so the post-loop empty-check is skipped. The executor returns. promiseAny returns a pending Promise.
Microtask after t = 0. fail's rejection callback runs. settled is false. We write errors[0] = 'first-input'. remaining becomes 1. Not zero, so we do NOT call failAll() — there's still one input we haven't heard from, and it might fulfil. We keep waiting.
t = 20ms. win's fulfilment callback runs. settled is still false. We set settled = true and call resolve('won!'). The Promise returned from promiseAny fulfils with 'won!'.
Two things to notice: the recorded errors[0] never reached the caller because the next event was a fulfilment, not another rejection — errors is only surfaced via AggregateError, and we only build one if remaining hits zero. And the function did exactly two Promise.resolve(...).then(...) attachments — O(n) work — regardless of how the promises interleaved.
errors.push(reason) instead of errors[slot] = reason. Push appends in settle order. If your inputs are [slow-reject, fast-reject] and the fast one rejects first, errors comes out reversed relative to input. A caller doing errors[i].retry(input[i]) reads the wrong reason against the wrong input. Always assign into a fixed slot captured at registration time.if (errors.length === inputs.length) instead of a remaining counter. Assigning errors[2] = reason when errors[0] and errors[1] are still empty makes .length jump from 0 to 3. You'll throw the AggregateError early with [undefined, undefined, reason] even though two inputs are still pending. Track count separately.settled guard on rejection. Once a fulfilment has won, a later rejection should be silently dropped — not written into errors, not counted, nothing. If you skip the guard you'll mutate state after the promise has already resolved, which is wasted work at best and a memory leak at worst (errors keeps growing for an aggregate nobody will see).new AggregateError(errors) instead of rejecting. AggregateError is the reason, not the value. reject(new AggregateError(...)) makes the returned promise reject; resolve(new AggregateError(...)) would make it fulfil with an Error object, which a caller's .then would receive as a plain value. Use reject.'All promises were rejected'. If you write 'all promises rejected' or anything else, frameworks and tests that string-match on the message break. Pass it as the second argument to the AggregateError constructor — that's the message slot.new AggregateError([], 'All promises were rejected'). Don't resolve(undefined).Promise.all (the inverse). Same scaffolding (new Promise, iterate, attach .then, slot-indexed array, remaining counter) but opposite polarity: results[slot] = value on fulfilment, reject(reason) immediately on rejection, resolve when remaining hits zero. The shape of the code is identical; only what you do on each branch flips.Promise.allSettled. Never short-circuits — waits for every input regardless. Instead of errors or results, you build a single outcomes[slot] = { status: 'fulfilled', value } or { status: 'rejected', reason } on each settlement. The settled latch goes away entirely, since nothing short-circuits.promiseAny([...inputs, rejectAfter(ms)]) where rejectAfter returns a promise that rejects after ms milliseconds. If any input wins before the timer, you get its value; if the timer wins, the AggregateError includes the timer's rejection alongside every other failure — telling you exactly which mirrors were still in flight when you gave up. A small but powerful pattern for "fastest mirror with a deadline."Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.