Promise Combinators
Real applications rarely await a single asynchronous task in isolation—you fetch several resources at once, race a request against a timeout, or kick off many jobs and report on each. The Promise class ships four static combinators that take an iterable of promises and return one new promise summarising the group. Choosing the right combinator is the difference between code that fails fast, code that tolerates partial failure, and code that finishes the moment the first result lands. This page covers Promise.all, Promise.allSettled, Promise.race, and Promise.any.
All four accept any iterable (typically an array) and run the supplied promises concurrently—the promises are already in flight before you pass them in; the combinator only decides how to aggregate their outcomes.
Promise.all — fail fast on the first rejection
Promise.all waits for every promise to fulfil, then resolves with an array of results in the same order as the input (regardless of which finished first). If any promise rejects, the returned promise rejects immediately with that reason—it does not wait for the rest. Use it when you need all the results and a single failure means the whole operation is invalid.
const urls = [
"https://api.example.com/user",
"https://api.example.com/posts",
"https://api.example.com/settings",
];
const [user, posts, settings] = await Promise.all(
urls.map((url) => fetch(url).then((r) => r.json()))
);
console.log(user, posts, settings);
Because the result order matches the input order, destructuring like this is safe even though the requests resolve in an unpredictable sequence.
Gotcha:
Promise.allrejects on the first failure, but the other promises keep running—it does not cancel them. To actually abort in-flightfetchcalls, wire up anAbortControllerand abort the remaining requests in acatch.
Promise.allSettled — collect every outcome
Promise.allSettled never rejects (short of a synchronous bug). It waits for every promise to settle—fulfilled or rejected—and resolves with an array of result objects. Each object has a status of "fulfilled" (with a value) or "rejected" (with a reason). Reach for it when partial success is acceptable and you want to inspect each result individually.
const results = await Promise.allSettled([
Promise.resolve(42),
Promise.reject(new Error("network down")),
Promise.resolve("ok"),
]);
for (const r of results) {
if (r.status === "fulfilled") console.log("✓", r.value);
else console.log("✗", r.reason.message);
}
Output:
✓ 42
✗ network down
✓ ok
Promise.race — first to settle wins
Promise.race settles as soon as the first promise settles, adopting its state—fulfilled or rejected. The classic use case is imposing a timeout: race a real operation against a timer that rejects.
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timed out")), ms)
);
}
try {
const data = await Promise.race([
fetch("/slow-endpoint").then((r) => r.json()),
timeout(3000),
]);
console.log("got data", data);
} catch (err) {
console.error(err.message); // "Timed out" if fetch took >3s
}
Tip: Because
racesettles on the first outcome, an early rejection wins over a later fulfilment. If a rejection should not abort the race, usePromise.anyinstead.
Promise.any — first to fulfil wins
Promise.any (ES2021) resolves with the value of the first promise to fulfil, ignoring rejections. It only rejects if every promise rejects, and when it does the reason is an AggregateError whose .errors array holds all the individual reasons. It is ideal for redundancy—query several mirrors and take whichever responds successfully first.
try {
const fastest = await Promise.any([
fetch("https://mirror-1.example.com/data"),
fetch("https://mirror-2.example.com/data"),
fetch("https://mirror-3.example.com/data"),
]);
console.log("served by", fastest.url);
} catch (err) {
// All mirrors failed
console.error(err instanceof AggregateError, err.errors.length);
}
Comparison at a glance
| Combinator | Resolves when | Rejects when | Resolved value | Rejected reason |
|---|---|---|---|---|
Promise.all | all fulfil | any rejects | array of values (input order) | first rejection reason |
Promise.allSettled | all settle | never | array of {status, value/reason} | — |
Promise.race | first settles (fulfil) | first settles (reject) | first settled value | first settled reason |
Promise.any | first fulfils | all reject | first fulfilled value | AggregateError |
A quick way to remember it: all and allSettled wait for everyone; race and any finish early. Within each pair, all/race are sensitive to rejections, while allSettled/any shrug them off.
Best Practices
- Reach for
Promise.allwhen results are interdependent and any failure invalidates the whole batch—then handle the rejection in one place. - Prefer
Promise.allSettledfor fan-out work where partial success is useful, such as sending notifications or warming caches. - Use
Promise.racefor timeouts and cancellation patterns, pairing it with anAbortControllerto stop the work that lost the race. - Use
Promise.anyfor redundancy across mirrors or fallbacks; remember to unwrapAggregateError.errorswhen everything fails. - Build the array of promises before passing it in—calling the async functions starts the work; the combinator only aggregates it.
- Always attach a
catch(ortry/await) to combinators that can reject, otherwise you risk an unhandled rejection.