JAVASCRIPT / PROMISES AND ASYNC PATTERNS
Race, any, and allSettled for competing tasks
Choose between Promise.race, Promise.any, and Promise.allSettled for competing tasks, read AggregateError, and clean up the tasks that lose.
What you will learn
- Pick race for first settlement, any for first success, allSettled for every outcome
- Read AggregateError.errors when Promise.any rejects; it holds reasons in input order
- Branch on entry.status before touching value or reason in allSettled results
- Clear timers or abort requests yourself, because losers keep running
Understanding Race, any, and allSettled for competing tasks
Promise.all waits for every task and abandons the whole group on the first rejection. The three combinators here change what counts as the finish line instead. Promise.race takes the first settlement of any kind, so a fast rejection beats a slower success; Promise.any ignores rejections and takes the first fulfilment, failing only when nothing succeeds; Promise.allSettled picks no winner at all and reports every outcome once the last one settles.
None of the three stops the tasks that lose. A combinator only subscribes to promises, and a promise has no cancel button, so a lost POST still reaches the server and a lost timer still fires. That is why a race-based timeout needs clearTimeout and a race-based failover needs AbortController when the work is expensive. It also means a loser's later rejection is already considered handled by the combinator, so it vanishes without an unhandled-rejection warning.
The result shapes differ as much as the timing rules. race and any hand you a bare value, and any's failure is a single AggregateError whose errors array collects every reason in input order rather than rejection order. allSettled fulfills with descriptor objects, {status, value} or {status, reason}, also in input order, so settlement order decides who wins while array order decides how results are reported. Practically: put try/catch around race and any, and branch on status after allSettled instead of writing a catch that will never run.
const settleAfter = (ms, value, fail = false) =>
new Promise((resolve, reject) =>
setTimeout(() => (fail ? reject(new Error(value)) : resolve(value)), ms)
);
// one fast failure, two slower successes
const tasks = () => [
settleAfter(30, 'slow-ok'),
settleAfter(10, 'fast-fail', true),
settleAfter(20, 'mid-ok')
];
async function main() {
try {
console.log('race:', await Promise.race(tasks())); // not reached
} catch (err) {
console.log('race rejected:', err.message);
}
console.log('any:', await Promise.any(tasks()));
for (const r of await Promise.allSettled(tasks())) {
console.log('allSettled:', r.status, r.status === 'fulfilled' ? r.value : r.reason.message);
}
}
main();race, any, and allSettled differ only in which settlement they accept as the answer — the first of any kind, the first fulfilment, or all of them — and none of them stop the tasks that lose.
Worked examples
Timeout guard with race
Wraps any promise in a deadline and disposes of the timer whichever side wins.
function withTimeout(promise, ms) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
const work = (ms, label) => new Promise(resolve => setTimeout(() => resolve(label), ms));
(async () => {
console.log(await withTimeout(work(20, 'quick'), 50));
try {
await withTimeout(work(200, 'slow'), 50);
} catch (err) {
console.log('caught:', err.message);
}
console.log('done; the 200ms task still finishes unseen');
})();Example explained
Line 1Promise.race([promise, timeout]) settles with whichever branch finishes first, so the deadline only wins when the work is slower than ms.
Line 2.finally(() => clearTimeout(timer)) runs on both paths; without it the pending timeout timer would sit in the queue after the work already succeeded.
Line 3The timeout branch rejects with an ordinary Error, so try/catch around the await is enough — there is nothing special about a race rejection.
Line 4work(200, 'slow') is not cancelled by losing: its timer still fires later and its value is simply discarded.
any collecting every failure
Shows what Promise.any rejects with when no input ever fulfills.
const fail = (ms, msg) =>
new Promise((_, reject) => setTimeout(() => reject(new Error(msg)), ms));
Promise.any([fail(10, 'eu-west down'), fail(5, 'us-east down')])
.catch(err => {
console.log(err.name);
console.log(err instanceof AggregateError);
console.log(err.errors.map(e => e.message).join(' | '));
});Example explained
Line 1The 5ms rejection does not settle Promise.any, because a rejection is not an answer for any — it keeps waiting for a fulfilment.
Line 2Only when the last input rejects does it reject, and with one AggregateError rather than a single reason.
Line 3err.message is a generic string, so the detail you want lives in err.errors.
Line 4err.errors follows the input array order: us-east rejected first yet appears second.
Reading allSettled descriptors
Separates successes from failures in an allSettled result array.
const ping = (host, ms, ok) =>
new Promise((resolve, reject) =>
setTimeout(() => (ok ? resolve(`${host} ok`) : reject(new Error(`${host} refused`))), ms)
);
Promise.allSettled([ping('a', 15, true), ping('b', 5, false), ping('c', 10, true)])
.then(results => {
const ok = results.filter(r => r.status === 'fulfilled');
console.log('fulfilled count:', ok.length);
console.log('first entry:', results[0].status, results[0].value);
console.log('has value key on rejected?', 'value' in results[1]);
console.log('reason:', results[1].reason.message);
});Example explained
Line 1The handler is in .then even though ping('b', ...) rejected: allSettled fulfills, so a catch here would never run.
Line 2results keeps input order, so results[0] is the 15ms ping even though the 5ms one settled first.
Line 3'value' in results[1] is false because a rejected entry carries reason instead, and reading r.value blindly would give undefined.
Line 4results[1].reason is the original Error object, so .message still gives the text you threw.
Important notes
Empty input behaves differently in each: Promise.race([]) stays pending forever, Promise.any([]) rejects immediately with an AggregateError, and Promise.allSettled([]) fulfills with [].
Non-promise values count as already fulfilled, so a plain value slipped into Promise.race wins before any timer or request can — useful for a default, easy to do by accident.
Common mistakes
Using Promise.race when you mean 'first one that works': a 10ms rejection wins and your code throws even though a 20ms request would have succeeded seconds later.
Mapping over allSettled results with r.value: rejected entries have no value, so failures silently become undefined and no error is ever logged.
Building a race-based timeout without clearTimeout: the losing timer stays pending, keeps a Node process alive for the whole deadline, and later rejects into nothing.
Try it yourself
Change, predict, then run
In a browser console write task(ms, label, shouldFail) that settles via setTimeout, then feed the same array of three tasks (one fast rejection, two slower successes) to Promise.race, Promise.any, and Promise.allSettled and log each result. Then flip the fast task to succeed and predict which of the three logs changes before you run it.
Open the JavaScript workspaceCheck your understanding
Three requests start together: A rejects after 10ms, B fulfills after 20ms, C fulfills after 30ms. What does Promise.race([A, B, C]) do, and what happens to the other two?
- It fulfills at 20ms with B's value, because a combinator ignores rejections.
- It waits for all three and rejects with an AggregateError holding A's reason.
- It rejects at 10ms with A's reason, and B and C still run to completion.
- It rejects at 10ms with A's reason and B and C are cancelled, so their work stops.
Show answer
race adopts the first settlement of any kind, and A's rejection at 10ms is that settlement; nothing is cancelled, because the combinator only subscribes to the promises, so B and C finish and their values are discarded. Option 0 describes Promise.any, which is the function that skips rejections while waiting for a fulfilment.