Implement Promise.any
Promise.any resolves with the first input that fulfils, and rejects only if every input rejects. Resolve on the first success, collect rejection reasons by index, and when the count of rejections reaches the input length, reject with an AggregateError carrying them in input order.
The question
Almost always straight after Promise.all, because it’s the mirror image and the
contrast is the point. Two details carry the marks: rejecting with an AggregateError
rather than a plain Error, and rejecting empty input immediately instead of hanging.
The solution
Structurally the inverse of Promise.all: success short-circuits, failure
accumulates.
function promiseAny(iterable) {
return new Promise((resolve, reject) => {
const items = Array.from(iterable);
const errors = new Array(items.length);
let remaining = items.length;
if (remaining === 0) {
reject(new AggregateError([], "All promises were rejected"));
return;
}
items.forEach((item, index) => {
Promise.resolve(item).then(resolve, (error) => {
// Reasons are reported in input order, not rejection order.
errors[index] = error;
remaining -= 1;
if (remaining === 0) {
reject(new AggregateError(errors, "All promises were rejected"));
}
});
});
});
}
How it works
resolveis passed straight through as the fulfilment handler, so the first success settles the outer promise and every later one is ignored.- Errors are stored by index, so
error.errorsreports them in input order rather than the order they happened to fail. - The counter reaching zero means everything rejected, which is the only condition under which
Promise.anyrejects. - Empty input rejects immediately with an
AggregateErrorand no errors, which is what the specification requires and what the medium test checks.
The tests it has to pass
This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 4 cases Practice Pad runs against your solution, and the implementation above passes all of them.
✓ resolves with the first value that fulfils
const winner = await promiseAny([
new Promise((resolve) => setTimeout(() => { resolve("later"); }, 40)),
Promise.resolve("cached"),
]);
expectEqual(winner, "cached");
✓ resolves with the fastest of several pending promises
const at = (ms, value) => new Promise((resolve) => setTimeout(() => { resolve(value); }, ms));
expectEqual(await promiseAny([at(60, "slow"), at(10, "quick"), at(40, "middle")]), "quick");
✓ rejects empty input with an AggregateError
let isAggregate = false;
try {
await promiseAny([]);
} catch (error) {
isAggregate = error instanceof AggregateError;
}
expectEqual(isAggregate, true);
✓ keeps every rejection reason in input order when all inputs reject
let messages;
try {
await promiseAny([Promise.reject(new Error("first")), Promise.reject(new Error("second"))]);
} catch (error) {
if (error instanceof AggregateError) {
messages = error.errors.map((item) => (item instanceof Error ? item.message : "?"));
}
}
expectEqual(messages, ["first", "second"]);
Edge cases to watch out for
Rejecting with a plain Error
The specification is specific: it must be an AggregateError, whose errors property holds every reason. The test does an instanceof check, and so do real callers.
Resolving empty input instead of rejecting
Promise.all([]) fulfils with [], which makes people assume Promise.any([]) does something similar. It rejects, because no input fulfilled and none ever will.
Collecting errors in rejection order
Pushing as they arrive gives you the order they failed in, which isn’t the input order the specification promises. Assign by index.
Confusing it with Promise.race
race settles with the first promise to settle either way, so a fast rejection wins. any ignores rejections until every one has failed. They behave identically only when the first settlement is a success.
Follow up questions
What’s the difference between Promise.any and Promise.race?
race settles with whichever promise settles first, success or failure, so one fast rejection beats a slower success. any ignores rejections and waits for the first fulfilment, rejecting only if every input rejects.
What is AggregateError?
A built-in error type that wraps several errors in one, exposing them on an errors array. It exists mainly for Promise.any, which needs to report why every input failed rather than picking one arbitrarily.
How do you stop Promise.any waiting forever?
Race it against a timeout, or include a promise that rejects after a delay as one of the inputs. The second is neater: because any only rejects when everything rejects, a timed rejection joins the aggregate rather than overriding a real success that arrives late.
When would you actually use Promise.any?
When several sources can answer the same question and you want the first that works: querying mirrors or CDN endpoints, racing a cache against a network fetch, or trying several fallback providers. You want a success, and you only care about the failures if they all fail.
Practise this with the tests running
Practice Pad runs these 4 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.
Download for macOS Free. Requires macOS 12 or later on Apple Silicon.