A dashboard loads the user, their orders and some recommendations. The recommendations service has a bad afternoon, and suddenly the whole page shows an error, even though the two requests that mattered came back fine. That’s Promise.all doing exactly what it promises. Promise.all vs allSettled vs race vs any is really one question: what should happen when some of your async work fails? This guide answers it with code you can run and the real output it prints, then covers the patterns you need around them in production: timeouts, retries and concurrency limits.
The four Promise combinators at a glance
All four take an array (any iterable, really) of promises and return one promise. They differ in when that promise settles and with what:
Promise.allfulfills with every value, in input order, once all succeed. It rejects as soon as any one rejects.Promise.allSettledwaits for every promise and never rejects. You get a status object for each.Promise.anyfulfills with the first success. It rejects only if every promise rejects.Promise.racesettles like whichever promise settles first, success or failure.
Every example below uses this stand-in for fetch, so you can run them with nothing but Node (22.18+ runs .ts files directly):
// fake.ts — a stand-in for fetch that succeeds or fails after a delay
export const wait = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
export async function fakeFetch(name: string, ms: number, fail = false) {
await wait(ms);
if (fail) throw new Error(`${name} failed`);
return name;
}
const start = Date.now();
export const at = () => `${Date.now() - start}ms`;Promise.all: all or nothing
Use Promise.all when you need every result and one failure makes the rest useless: the data a page cannot render without, or the steps of a checkout.
// all.ts
import { fakeFetch, at } from "./fake.ts";
try {
const results = await Promise.all([
fakeFetch("user", 100),
fakeFetch("orders", 200, true), // this one fails
fakeFetch("recommendations", 300),
]);
console.log(results);
} catch (err) {
console.log(`rejected at ${at()}: ${(err as Error).message}`);
}
// Output:
// rejected at 200ms: orders failedNotice the timing: it rejected at 200 ms, the moment orders failed, without waiting for the 300 ms request. That fail-fast behaviour is the point. Two details people get wrong:
The other promises keep running.
Promise.allstops waiting, it doesn’t cancel anything. Requests still complete and side effects still happen.Later rejections are swallowed quietly. Because
Promise.allattached a handler to every promise, a second failure doesn’t crash Node with an unhandled rejection. It’s just ignored:
// failfast.ts
import { wait } from "./fake.ts";
async function job(name: string, ms: number, fail = false) {
await wait(ms);
console.log(`${name} finished`);
if (fail) throw new Error(`${name} failed`);
}
await Promise.all([job("a", 100, true), job("b", 300), job("c", 400, true)])
.catch((err: Error) => console.log("caught:", err.message));
await wait(500);
console.log("done, and no unhandled rejection for c");
// Output:
// a finished
// caught: a failed
// b finished
// c finished
// done, and no unhandled rejection for cJobs b and c ran to completion after the rejection, and c’s error vanished. If you need to know about every failure, you want the next combinator.
Promise.allSettled: get every result, failures included
Use allSettled when the tasks are independent and partial success is still success: dashboard widgets, a batch of emails, uploading several files where you report which ones failed.
// all-settled.ts
import { fakeFetch, at } from "./fake.ts";
const results = await Promise.allSettled([
fakeFetch("user", 100),
fakeFetch("orders", 200, true),
fakeFetch("recommendations", 300),
]);
console.log(`settled at ${at()}`);
for (const r of results) {
if (r.status === "fulfilled") console.log("ok:", r.value);
else console.log("failed:", (r.reason as Error).message);
}
// Output:
// settled at 301ms
// ok: user
// failed: orders failed
// ok: recommendationsIt waited for the slowest request and never threw. Each result is { status: "fulfilled", value } or { status: "rejected", reason }, and in TypeScript checking status narrows the type, so r.value is only reachable on success. That’s the fix for the dashboard from the intro: render the widgets that loaded, show a small error state for the one that didn’t.
One habit to build: allSettled never rejects, so it will never tell you anything went wrong unless you look. Always handle the rejected entries, even if that only means logging them.
Promise.any: first success wins
Use any when you have several ways to get the same thing and only need one: mirrors or CDNs, a cache and a network fallback, several geocoding providers.
// any.ts
import { fakeFetch } from "./fake.ts";
const fastest = await Promise.any([
fakeFetch("mirror-a", 300),
fakeFetch("mirror-b", 100, true), // fastest, but fails
fakeFetch("mirror-c", 200),
]);
console.log("any:", fastest);
try {
await Promise.any([fakeFetch("a", 50, true), fakeFetch("b", 80, true)]);
} catch (err) {
if (err instanceof AggregateError) {
console.log(err.message);
console.log(err.errors.map((e: Error) => e.message));
}
}
// Output:
// any: mirror-c
// All promises were rejected
// [ 'a failed', 'b failed' ]The fastest mirror failed, so any ignored it and took the next success. When everything fails you get an AggregateError whose errors array holds every reason, in input order. That message comes from V8, the engine in Node and Chrome; Firefox and Safari word it differently, so branch on instanceof AggregateError, never on the text.
Promise.race: first to settle, success or failure
race is the one people reach for most and need least. It settles with whatever happens first, including an error:
// race.ts
import { fakeFetch } from "./fake.ts";
const winner = await Promise.race([
fakeFetch("slow", 300),
fakeFetch("broken", 100, true),
]).catch((err: Error) => `race rejected: ${err.message}`);
console.log(winner);
// Output:
// race rejected: broken failedA fast failure beats a slow success. If you wanted “first one that works”, that’s Promise.any. The classic use of race is a timeout, racing your work against a timer that rejects. It works, but it has a hidden cost: the losing request keeps running and keeps its connection open. For fetch, there’s a better tool.
How to add a timeout to fetch
AbortSignal.timeout(ms) gives you a signal that aborts itself, and passing it to fetch actually cancels the request instead of just ignoring it. To see it work, this script starts a local server that takes two seconds to answer:
// timeout.ts — a local server that takes 2 s to answer
import { createServer } from "node:http";
const server = createServer((_req, res) => {
setTimeout(() => res.end("finally"), 2000);
}).listen(4567);
try {
const res = await fetch("http://localhost:4567", {
signal: AbortSignal.timeout(500),
});
console.log(await res.text());
} catch (err) {
const e = err as DOMException;
console.log(`${e.name}: ${e.message}`);
}
// User cancel OR timeout, whichever comes first.
const userCancel = new AbortController();
setTimeout(() => userCancel.abort(), 100);
try {
await fetch("http://localhost:4567", {
signal: AbortSignal.any([userCancel.signal, AbortSignal.timeout(500)]),
});
} catch (err) {
console.log((err as DOMException).name);
}
server.closeAllConnections();
server.close();
// Output:
// TimeoutError: The operation was aborted due to timeout
// AbortErrorTwo different error names, and that’s useful: TimeoutError means the server was too slow (show “try again”), while AbortError means you or the user cancelled on purpose (show nothing). AbortSignal.any combines signals, which is how a search box can cancel on a new keystroke and give up after a few seconds. Both are covered on MDN’s AbortSignal.timeout page, and both are available in current browsers and Node.
The waterfall: sequential awaits that should be parallel
The most common async performance bug isn’t picking the wrong combinator. It’s not using one at all:
// waterfall.ts
import { fakeFetch } from "./fake.ts";
let t = Date.now();
const user = await fakeFetch("user", 200);
const posts = await fakeFetch("posts", 300);
console.log(`sequential: ${Date.now() - t}ms`, user, posts);
t = Date.now();
const [user2, posts2] = await Promise.all([
fakeFetch("user", 200),
fakeFetch("posts", 300),
]);
console.log(`parallel: ${Date.now() - t}ms`, user2, posts2);
// Output:
// sequential: 501ms user posts
// parallel: 300ms user postsTwo independent requests awaited one after the other cost the sum of their times; in parallel they cost the longest. The same rule applies in Next.js Server Components, where an async page that awaits three fetches in a row makes the whole render wait for all three in sequence. My post on Server Components in practice covers fetching there. Only keep awaits sequential when the second request genuinely needs the first one’s result.
The forEach trap
The other everyday version of this bug: forEach doesn’t wait for async callbacks.
// foreach.ts
import { fakeFetch } from "./fake.ts";
const ids = ["a", "b", "c"];
const saved: string[] = [];
ids.forEach(async (id) => {
saved.push(await fakeFetch(id, 100));
});
console.log("after forEach:", saved);
const all = await Promise.all(ids.map((id) => fakeFetch(id, 100)));
console.log("after Promise.all:", all);
// Output:
// after forEach: []
// after Promise.all: [ 'a', 'b', 'c' ]forEach calls your async function, gets back a promise, throws it away and moves on. Map to promises and hand them to a combinator instead, or use a plain for...of loop with await when the items must run one at a time.
How to limit concurrency
Promise.all over 500 items starts 500 requests at once, which is how you get rate-limited or knock over your own API. A small worker pool caps how many run at the same time while keeping results in input order:
// pool.ts
export async function mapWithLimit<T, R>(
items: T[],
limit: number,
fn: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
const results = new Array<R>(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const i = next++;
results[i] = await fn(items[i], i);
}
}
await Promise.all(
Array.from({ length: Math.min(limit, items.length) }, worker),
);
return results;
}It works because JavaScript is single-threaded: next++ can’t be interrupted, so two workers never grab the same index. Running ten 100 ms uploads through it with a limit of 3 finished in about 400 ms with never more than 3 in flight. That’s four rounds instead of one burst of ten. Like Promise.all, it stops at the first error; wrap fn in a try/catch if you want allSettled-style results.
How to retry a failed promise with backoff
Some failures are temporary: a 503, a dropped connection, a cold server. Retrying immediately in a tight loop makes them worse, so wait longer after each attempt and add a little randomness so a thousand clients don’t retry in lockstep:
// retry.ts
export async function retry<T>(
fn: () => Promise<T>,
{ attempts = 3, baseMs = 200 } = {},
): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= attempts) throw err;
// 200 ms, 400 ms, 800 ms… plus jitter so clients don't retry in sync
const delay = baseMs * 2 ** (attempt - 1) * (0.5 + Math.random());
await new Promise((r) => setTimeout(r, delay));
}
}
}In my test, a function that failed twice with a 503 succeeded on the third attempt, and one that never recovered threw its last error after three tries. Only retry what’s safe to repeat: reads, and writes your API makes idempotent. Retrying a non-idempotent “charge card” call is how customers get billed twice.
Pick the combinator by what a failure should mean: stop everything, report it, try another source, or give up waiting.
Which one should you use?
Every result is required, and one failure ruins the rest?
Promise.all.Independent tasks where partial success is fine?
Promise.allSettled, and handle the failures.Several sources for the same thing, and you need one?
Promise.any.Need a timeout? For
fetch,AbortSignal.timeout. For other work,Promise.raceagainst a timer, knowing the loser keeps running.Hundreds of items? A concurrency limit around whichever of the above fits.
Frequently Asked Questions
What is the difference between Promise.all and Promise.allSettled?
Promise.all rejects as soon as any input promise rejects and only gives you values when every promise succeeds. Promise.allSettled waits for every promise and never rejects, returning a { status, value } or { status, reason } object for each. Use all when one failure makes the whole operation useless, and allSettled when the tasks are independent.
Does Promise.all cancel the other promises when one fails?
No. Promise.all stops waiting at the first rejection, but the other operations keep running to completion, and their later errors are silently ignored. JavaScript promises can’t be cancelled; to actually stop work like a fetch, pass an AbortSignal and abort it.
What is the difference between Promise.race and Promise.any?
Promise.race settles with the first promise to settle, so a fast failure wins over a slow success. Promise.any ignores failures and fulfills with the first success, rejecting with an AggregateError only when every promise fails. For “use whichever source answers first”, Promise.any is almost always the one you want.
How do I run promises in parallel in JavaScript?
Start the async calls without awaiting them one at a time, then await them together, for example await Promise.all([getUser(), getPosts()]). Two sequential awaits cost the sum of both durations, while running them together costs only the longest. For large lists, add a concurrency limit so you don’t start hundreds of requests at once.
The short version
Decide what a failure should mean before you pick the tool. all when it should stop everything, allSettled when it should be reported, any when another source can cover it, and an AbortSignal when waiting too long counts as failing. Then check your code for the two everyday bugs, the sequential waterfall and the async forEach. If promises and async/await still feel shaky, start with the JavaScript concepts to know before React, and find more in the JavaScript articles.



