Picture a page that loads a user, their orders and their recommendations with Promise.all. The recommendations service hiccups, and the whole page shows an error, even though the user and orders loaded fine.
Nothing is broken. You asked a question with the wrong rule for waiting.
The mental model: four rules for when to stop waiting
Picture yourself waiting for friends to reply in a group chat about dinner. All four Promise APIs send the message to everyone at once. They differ only in when you stop checking the chat:
-
all: wait until everyone says yes. If anyone says no, the dinner is off right away. -
race: the first reply decides, yes or no. -
any: the first yes decides. You only give up if everyone says no. -
allSettled: wait until everyone has answered, then read the full list.
Choose a Promise combinator by what should count as failure, not by speed. They all run the work concurrently; the difference is when you stop waiting.
How each one decides
Promise.all: all or nothing. Resolves with every value in order, or rejects with the first error.
const [user, orders] = await Promise.all([getUser(), getOrders()]);
await Promise.all([ok(), fail(), ok()]); // rejects with fail()'s error
Promise.race: first to settle wins, whether it succeeded or failed. That makes it the classic timeout:
await Promise.race([fetch("/data"), timeout(3000)]); // whichever finishes first
Promise.any: first success wins. Failures are ignored until all of them fail; then it rejects with an AggregateError that holds every reason.
await Promise.any([fail(), ok("cdn-2"), ok("cdn-3")]); // "cdn-2"
Promise.allSettled: never rejects. It waits for everyone and gives you a report:
await Promise.allSettled([ok(1), fail()]);
// [{ status: "fulfilled", value: 1 }, { status: "rejected", reason: Error }]
In practice
Back to the dinner chat: ask yourself who's allowed to say no.
| You need… | Use |
|---|---|
| Every result, or nothing makes sense | all |
| A deadline or "fastest answer, good or bad" | race |
| One working answer from several backups | any |
| As many results as possible, failures included | allSettled |
For the page in the hook, the user and orders are required, but recommendations are optional. So split them: all for the required pair, and handle recommendations on their own (or use allSettled and render what came back).
The honest stakes: all is right most of the time. The bugs show up when you use it for optional data, or use race when you meant "first success" (a fast failure wins the race and you reject too early).
Say it in an interview
"All four start every promise at once; none of them make JavaScript parallel. They only differ in when they stop waiting.
allneeds everyone to succeed and fails on the first error.racetakes the first result, success or failure, so it's good for timeouts.anytakes the first success and only fails if all fail.allSettlednever fails; it waits for everyone and reports each outcome."
Cheat sheet
| API | Stops waiting when | Rejects? |
|---|---|---|
all |
everyone succeeds, or first failure | yes, first error |
race |
first one settles | if the first one failed |
any |
first success | only if all fail (AggregateError) |
allSettled |
everyone settles | never |
- Same start, different stop rule.
- Pick by who's allowed to fail.
-
race≠any: a fast failure wins a race.
Going deeper
Advanced and optional. Everything above is enough to explain this in an interview. This part is for the curious: full numbers, edge cases and what's still unproven.
Stopping early doesn't cancel anything
When all rejects or race settles, the other promises keep running. Their requests still go out, and their results are just ignored. To actually stop the work, pass an AbortController signal to fetch and call abort() once you have your answer. See AbortController, a practical guide.
Sequential vs concurrent
The combinators only help if you start the work before you wait for it:
await a(); await b(); // sequential: b starts after a finishes
await Promise.all([a(), b()]); // concurrent: both start right away
Why this works on a single thread is its own story: How Promise.all works in a single-threaded world.
Edge cases
- Empty input:
all([])andallSettled([])resolve to[],any([])rejects with anAggregateError, andrace([])stays pending forever. - Non-promise values are wrapped with
Promise.resolve, soPromise.all([1, fetchX()])works. -
Promise.anyandAggregateErrorare ES2021;allSettledis ES2020. Both are in every current browser and Node 16+.
Originally published on Bitlyst.
Top comments (0)