Lesson 26·M3 Async JavaScript
Promise Combinators
OpenLearning Objectives
By the end of this lesson you should be able to:
- State the settlement rule of
all,allSettled,raceandanyin one sentence each - Explain why
allfailing fast can be a footgun - Pick the right combinator from a scenario (parallel requests, partial failure, timeout, first-success)
- Implement a timeout with
Promise.race - Explain
AggregateErrorfromanyandall
What are Promise Combinators?
Static methods that take an iterable of promises and return a single promise that settles according to one rule.
all → settle when EVERY promise fulfils, or on the FIRST rejection
allSettled → settle when EVERY promise settles (fulfilled or rejected)
race → settle on the FIRST settlement of any kind
any → settle on the FIRST fulfilment, or with an AggregateError if ALL reject
One input, one output — the rule is the only thing you need to memorize.
Mental Model
Each combinator is a venue with a different finish line:
| Combinator | Finish line | Watch out |
|---|---|---|
all | Needs everyone to finish OK | One failure and it bails early |
allSettled | Waits for everyone, whatever happens | You inspect each result yourself |
race | First to cross the line, win or lose | A fast failure loses to a slow success |
any | First to cross OK | All failures → AggregateError |
Visual Flow
Promise.all([p1, p2, p3])
p1 ✅ ─┐
p2 ✅ ─┼─► [v1, v2, v3] one rejection anywhere ──► reject(firstError)
p3 ✅ ─┘
Promise.allSettled([p1, p2, p3])
p1 ✅ ─┐
p2 ❌ ─┼─► [{status:'fulfilled'}, {status:'rejected'}, …] always waits for all
p3 ✅ ─┘
Promise.race([p1, p2, p3]) first settlement, good or bad
────────► p2 ❌ settles first ──► reject
Promise.any([p1, p2, p3]) first FULFILMENT
────────► p1 ✅ settles first ──► resolveHow It Works
The mechanics of each, precisely.
Promise.all — fail fast. It resolves with an array of values in input order, and
rejects with the first rejection — even if other promises are still pending:
const p1 = Promise.resolve('a');
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('api down'), 5));
const p3 = new Promise((resolve) => setTimeout(() => resolve('c'), 30));
Promise.all([p1, p2, p3])
.then((all) => console.log('all:', all))
.catch((err) => console.log('caught:', err));
Output:
caught: api down
p3 is still running when p2 rejects — all doesn't wait for it. That's the footgun:
one flaky request takes down the whole batch.
Promise.allSettled — wait for everyone. Never rejects; every result carries its own
status:
const p1 = Promise.resolve('ok');
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('boom'), 5));
Promise.allSettled([p1, p2]).then((results) => console.log(results));
Output:
[
{ status: 'fulfilled', value: 'ok' },
{ status: 'rejected', reason: 'boom' }
]
Promise.race — first to settle, good or bad:
const fast = new Promise((resolve) => setTimeout(() => resolve('fast'), 5));
const slow = new Promise((resolve) => setTimeout(() => resolve('slow'), 40));
Promise.race([fast, slow]).then((winner) => console.log('race winner:', winner));
Output:
race winner: fast
Promise.any — first to fulfil; all reject → AggregateError:
const e1 = Promise.reject('e1');
const e2 = new Promise((resolve, reject) => setTimeout(() => reject('e2'), 5));
Promise.any([e1, e2]).catch((err) => {
console.log('any name:', err.name);
console.log('any errors:', err.errors);
});
Output:
any name: AggregateError
any errors: [ 'e1', 'e2' ]Real Project Usage
| Scenario | Combinator |
|---|---|
| Load two dashboards and render only when both arrive | all |
| Sync 50 rows; a few will 404 — keep the rest | allSettled |
Timeout a slow request (race against a timer) | race |
| Try CDN A, then CDN B, then fallback | any |
| Strict validation: all checks must pass | all |
| Prefetch images; the page renders even if one fails | allSettled |
The timeout pattern — race is the only combinator you'd hand-roll:
function withTimeout(promise, ms, label) {
return Promise.race([
promise,
new Promise((resolve, reject) =>
setTimeout(() => reject(new Error(`${label} timed out`)), ms)
),
]);
}
const slowFetch = new Promise((resolve) => setTimeout(() => resolve('data'), 100));
withTimeout(slowFetch, 30, 'GET /stats')
.then((data) => console.log(data))
.catch((err) => console.log(err.message));
Output:
GET /stats timed outInterview Explanation
Promise.allsettles when every promise fulfils — array of values in input order — and rejects on the first rejection.allSettledwaits for every promise and reports each result with its own status.racesettles on the first settlement of any kind.anysettles on the first fulfilment, or rejects with anAggregateErrorif all reject.
Senior-Level Insights
The four rules are table stakes. Senior-level answers add why:
- Fail-fast is a design decision, not a default.
allbails on the first rejection, so one flaky request can hide the useful data in the other three. When partial success is acceptable,allSettledgives you everything and you decide the policy in one place. racefor timeout is one-sided. The slow promise keeps running and can still reject later, becoming an unhandled rejection. A realAbortSignal.timeout()cancels the work instead of just ignoring it — prefer that when the API supports it.anyis for redundancy. "Try the next source" — the fast-path of failover logic.- Combinators compose with
async(Lesson 25).await Promise.allSettled([…])inside an async function is the idiomatic way to run independent work without a fail-fast surprise.
Common Mistakes
❌ Using all where a partial failure is fine — one 404 rejects the whole batch:
const results = await Promise.all(urls.map(fetch)); // ❌ one 404 kills all
❌ Using allSettled when every promise must succeed — you now write the error
handling by hand.
❌ Using race for timeouts without handling the loser — the slower promise's later
rejection can surface as an unhandled rejection.
❌ Expecting any to be "like race" — race settles on the first rejection too;
any ignores rejections unless all reject.
❌ Forgetting all preserves input order — it does, even when promises settle out of
order.
Best Practices
✅ Reach for allSettled when any promise may reject and the others still matter
✅ Use all for strict all-or-nothing batches (form validation, parallel must-haves)
✅ Implement timeouts with race (or AbortSignal.timeout() where supported)
✅ Use any for "first working source" — CDNs, replicas, fallbacks
✅ Wrap combinator results in try/catch or a .catch (Lesson 27)
❌ Don't let all's fail-fast hide recoverable failures in production data paths
Interview Questions
Q1. What's the difference between all and allSettled?
allsettles when every promise fulfils and rejects on the first rejection — fail fast.allSettledalways waits for every promise and returns an array of{status, value | reason}results, so it never rejects.
Q2. When would you use race?
When the first settlement decides — most commonly timeouts:
racethe real promise against a timer that rejects. Also first-available responses where a slow success can lose to a fast one.
Q3. What does Promise.any return if all promises reject?
An
AggregateErrorwhose.errorsarray holds every rejection reason.
Q4. You have 10 requests and 2 will fail. Which combinator?
allSettled, because I want the 8 successful results and the 2 reasons, not a batch-wide failure. WithallI'd lose everything to the first rejection.
Senior follow-up: What's wrong with a race-based timeout?
The loser keeps running. If the real request later rejects, its rejection is unhandled and can crash or warn globally. A cancellation-based timeout —
AbortControllerorAbortSignal.timeout()— stops the work, which is why fetch supports it.
Follow-up Questions
Q1. Does all run the promises in parallel?
They were already started — the promises are created before
allis called, so their work began in the previous line.allonly observes and combines them.
Q2. What order does all put the results in?
Input order, regardless of settlement order.
Q3. What if you pass an empty array to each?
allandallSettledresolve immediately;racestays pending forever;anyrejects with anAggregateErrorwith no errors.
Code Example: Dashboard That Tolerates Failure
function fetchStats() {
return new Promise((resolve, reject) =>
setTimeout(() => resolve({ visits: 120 }), 10)
);
}
function fetchRecent() {
return new Promise((resolve, reject) =>
setTimeout(() => reject(new Error('recent unavailable')), 5)
);
}
async function renderDashboard() {
const [stats, recent] = await Promise.allSettled([fetchStats(), fetchRecent()]);
const visits = stats.status === 'fulfilled' ? stats.value.visits : '—';
const posts = recent.status === 'fulfilled' ? recent.value : ['(empty)'];
return { visits, posts };
}
renderDashboard().then((d) => console.log(d));
Output:
{ visits: 120, posts: [ '(empty)' ] }Performance Notes
- When it matters:
allvs sequentialawaits is the difference between worst-case latency ofmax(...)vssum(...). Starting N requests in one tick and combining is the point of the combinator. - When it doesn't: a handful of calls where one dependency chain is inherently serial.
race-based timeouts don't stop the underlying work — if the timed-out promise holds a connection or a timer, that resource lives on until it settles. Cancellation APIs are the real fix.
Debugging Scenarios
| Symptom | Likely cause |
|---|---|
all rejects though most calls succeeded | One promise rejected — check each source; consider allSettled |
allSettled results are {status, value}/{status, reason} shapes | That's the contract — you must branch on status |
race "wins" with a rejection you expected to be late | First settlement wins, even if it's an error — use any for first success |
any throws AggregateError | Every promise rejected — inspect err.errors |
| Results out of order | You mapped after settling, or expected all to return settlement order |
Quick Revision Notes
all: all fulfil →[values]in input order; first rejection wins (fail fast)allSettled: waits for everyone; never rejects;{status, value | reason}per resultrace: first settlement of any kind decidesany: first fulfilment decides; all rejected →AggregateError- Promises are created (and start) before the combinator runs — combinators only observe
- Timeouts:
raceagainst a rejecting timer, orAbortSignal.timeout()for real cancellation
Cheat Sheet
Promise.all([p1, p2]).then(([a, b]) => a + b); // all or nothing, fail fast
Promise.allSettled([p1, p2]); // every outcome, never rejects
Promise.race([p, timeout(3000)]); // first settlement wins
Promise.any([cdnA, cdnB, fallback]); // first success wins
// timeout helper
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
]);
}Key Takeaways
Recap
- Four combinators, four rules:
allfail-fast,allSettledwaits for all,racefirst settlement,anyfirst fulfilment allreturns values in input order and rejects with the first rejectionallSettlednever rejects — each result carries its ownstatus(Lesson 25'sawaitloves this)raceis the timeout tool;anyis the failover toolanyall-rejected ⇒AggregateErrorwith.errors- Promises start before the combinator — these methods only combine, they don't launch
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/6State the settlement rule of each of the four combinators in one sentence.
Why is all called "fail fast", and when is that the wrong behaviour?
You have 12 sync requests and 3 will fail. Which combinator, and what do you do with the results?
Implement withTimeout(promise, ms) with race. What happens to the loser?
What does Promise.any reject with, and what's inside it?
When are the promises in Promise.all([…]) actually started?
What's Next
Lesson 27 — Error Handling & Propagation. How errors cross async boundaries — the
try/catch inside async, the .catch at the end of a chain, and how a senior keeps an
unhandled rejection from ever reaching production.