Promise Combinators: all, race, allSettled, any
Real Node.js applications rarely await a single promise at a time. You fetch from several APIs, query multiple tables, or read a batch of files — and you want them to run concurrently rather than one after another. Promise combinators are the standard library’s tools for coordinating a group of promises and deciding how their collective result should be shaped. Picking the right one is the difference between fast, resilient code and code that silently swallows errors or stalls on a single slow dependency.
The four combinators at a glance
All four are static methods on the global Promise object. They take an iterable of promises (or plain values, which are treated as already-resolved) and return a single promise. What differs is when that promise settles and what it settles with.
| Combinator | Resolves when | Rejects when | Result value |
|---|---|---|---|
Promise.all | all fulfill | any rejects (fail-fast) | array of fulfilled values |
Promise.allSettled | all settle | never rejects | array of {status, value/reason} |
Promise.race | first settles (fulfill or reject) | first settles with a rejection | the first settled value/reason |
Promise.any | first fulfills | all reject | first fulfilled value / AggregateError |
A key point that trips people up: combinators do not start anything. The promises you pass in are already running the moment they were created. The combinator only observes them, so concurrency comes from creating all the promises up front and then awaiting the combinator.
Promise.all — fail fast on the happy path
Promise.all is the workhorse. Use it when you need every operation to succeed and you want their results as an ordered array. Order is preserved by position in the input, not by completion time. If any input promise rejects, Promise.all rejects immediately with that first reason — the others keep running but their results are discarded.
async function loadDashboard(userId) {
const [profile, orders, notifications] = await Promise.all([
fetch(`https://api.example.com/users/${userId}`).then((r) => r.json()),
fetch(`https://api.example.com/users/${userId}/orders`).then((r) => r.json()),
fetch(`https://api.example.com/users/${userId}/notifications`).then((r) => r.json()),
]);
return { profile, orders, notifications };
}
const data = await loadDashboard(42);
console.log(`Loaded ${data.orders.length} orders for ${data.profile.name}`);
Output:
Loaded 3 orders for Ada Lovelace
Warning: Because
Promise.allis fail-fast, a single rejected promise discards the values of siblings that already succeeded. If you need those partial results, reach forPromise.allSettledinstead.
Promise.allSettled — collect every outcome
When you want a complete report regardless of individual failures, use Promise.allSettled. It never rejects. Instead it resolves with an array of result descriptors: { status: "fulfilled", value } or { status: "rejected", reason }. This is ideal for batch jobs where one bad item shouldn’t sink the whole operation.
import { readFile } from "node:fs/promises";
const files = ["config.json", "missing.json", "data.json"];
const results = await Promise.allSettled(
files.map((name) => readFile(name, "utf8")),
);
for (const [i, result] of results.entries()) {
if (result.status === "fulfilled") {
console.log(`OK ${files[i]} (${result.value.length} bytes)`);
} else {
console.log(`FAIL ${files[i]}: ${result.reason.code}`);
}
}
Output:
OK config.json (128 bytes)
FAIL missing.json: ENOENT
OK data.json (512 bytes)
Promise.race — first to settle wins
Promise.race settles as soon as the first input promise settles — whether it fulfills or rejects. The classic use case is imposing a timeout: race the real work against a promise that rejects after N milliseconds.
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms),
);
}
async function fetchWithTimeout(url, ms = 2000) {
return Promise.race([fetch(url), timeout(ms)]);
}
try {
const res = await fetchWithTimeout("https://api.example.com/slow", 500);
console.log("Status:", res.status);
} catch (err) {
console.error(err.message);
}
Output:
Timed out after 500ms
Tip: For cancelling the underlying work (not just ignoring it), prefer an
AbortControllerwithAbortSignal.timeout().Promise.raceabandons the loser but does not stop it — the fetch keeps running in the background.
Promise.any — first success wins
Promise.any is the optimistic counterpart to race. It ignores rejections and resolves with the value of the first promise to fulfill. It only rejects if all inputs reject, in which case it throws an AggregateError whose .errors array holds every individual reason. This is perfect for querying redundant mirrors or fallbacks where you just need one to work.
const mirrors = [
"https://eu.cdn.example.com/file.bin",
"https://us.cdn.example.com/file.bin",
"https://ap.cdn.example.com/file.bin",
];
try {
const res = await Promise.any(mirrors.map((url) => fetch(url)));
console.log("Fastest mirror responded:", res.url);
} catch (err) {
// err is an AggregateError
console.error(`All mirrors failed (${err.errors.length} errors)`);
}
Output:
Fastest mirror responded: https://us.cdn.example.com/file.bin
Choosing the right combinator
A quick decision guide:
- Need all results and want to stop on the first failure →
Promise.all. - Need all outcomes including failures, never throwing →
Promise.allSettled. - Need the first to settle, e.g. a timeout guard →
Promise.race. - Need the first success, with fallbacks →
Promise.any.
CommonJS users: all four combinators are part of the JavaScript language, not a module, so they work identically with
require-based code — there is nothing to import.
Best Practices
- Create all promises before awaiting the combinator so they run concurrently; awaiting in a
forloop serializes them and defeats the purpose. - Use
Promise.allSettledfor batch processing where partial success is acceptable, then filter onstatusto separate wins from failures. - Always attach error handling to
Promise.all— an unhandled rejection from one promise will surface as the combinator’s rejection. - Pair
Promise.racetimeouts with anAbortControllerso the losing operation is actually cancelled, not just orphaned. - Inspect
AggregateError.errorswhenPromise.anyrejects to log why every option failed, rather than swallowing the failure. - Cap fan-out concurrency (with a pool or a library like
p-limit) when mapping over large arrays, so you don’t open thousands of sockets at once.