#Promises#Polyfill

Promise.allSettled / race / any Polyfills

Implement the three Promise utility variants: allSettled (never rejects), race (first to settle wins), and any (first to resolve wins).

By Pratik RaiMedium

Promise.allSettled, Promise.race and Promise.any are the three combinators people reach for after outgrowing Promise.all. Each exists because all makes one specific trade-off — fail fast, wait for everything — that is wrong for a large class of problems. Implementing all three together is the fastest way to internalise which one you actually need.

The decision table

CombinatorSettles whenRejects whenResult shape
allevery promise fulfilsany rejects, immediatelyarray of values
allSettledevery promise settlesneverarray of status objects
racethe first promise settlesif that first one rejectedsingle value
anythe first promise fulfilsall rejectsingle value

The two easy-to-confuse pairs: race versus any (race cares about the first to settle, any about the first to succeed), and all versus allSettled (fail-fast versus report-everything).

allSettled — never rejects

Use it when partial failure is acceptable and you want to know exactly what happened to each item. Loading six dashboard widgets where two might fail is the canonical case: all would blank the whole dashboard.

JSfile.javascript
1function allSettled(iterable) { 2 return new Promise((resolve) => { 3 const items = Array.from(iterable); 4 const results = new Array(items.length); 5 let remaining = items.length; 6 7 if (remaining === 0) return resolve([]); 8 9 items.forEach((item, index) => { 10 Promise.resolve(item).then( 11 (value) => { 12 results[index] = { status: 'fulfilled', value }; 13 if (--remaining === 0) resolve(results); 14 }, 15 (reason) => { 16 results[index] = { status: 'rejected', reason }; 17 if (--remaining === 0) resolve(results); 18 } 19 ); 20 }); 21 }); 22}

The outer promise takes no reject parameter at all — that is the entire design. Both branches record and decrement identically.

Reading the output is the part people get wrong. Every entry needs its status checked before touching .value:

JSfile.javascript
1const settled = await Promise.allSettled(tasks); 2const succeeded = settled.filter((r) => r.status === 'fulfilled').map((r) => r.value); 3const failed = settled.filter((r) => r.status === 'rejected').map((r) => r.reason);

race — first to settle, win or lose

JSfile.javascript
1function race(iterable) { 2 return new Promise((resolve, reject) => { 3 for (const item of iterable) { 4 Promise.resolve(item).then(resolve, reject); 5 } 6 }); 7}

Strikingly short, because promises are already single-assignment: once resolve or reject has been called, every later call is ignored. No bookkeeping required.

The classic use is a timeout:

JSfile.javascript
1const withTimeout = (promise, ms) => 2 Promise.race([ 3 promise, 4 new Promise((_, rej) => setTimeout(() => rej(new Error('Timeout')), ms)), 5 ]);

Two traps here. An empty iterable means the promise never settlesrace([]) hangs forever, silently. And the losers keep running; the timeout above rejects your promise but the underlying request continues to completion. Pair it with AbortController when the work has a cost.

any — first to succeed

any ignores rejections until every single promise has failed, then rejects with an AggregateError carrying all the reasons.

JSfile.javascript
1function any(iterable) { 2 return new Promise((resolve, reject) => { 3 const items = Array.from(iterable); 4 const errors = new Array(items.length); 5 let remaining = items.length; 6 7 if (remaining === 0) { 8 return reject(new AggregateError([], 'All promises were rejected')); 9 } 10 11 items.forEach((item, index) => { 12 Promise.resolve(item).then(resolve, (error) => { 13 errors[index] = error; 14 if (--remaining === 0) { 15 reject(new AggregateError(errors, 'All promises were rejected')); 16 } 17 }); 18 }); 19 }); 20}

resolve is passed directly as the fulfilment handler — the first success wins and subsequent calls are no-ops. Errors are collected by index so the AggregateError.errors array lines up with the input.

Use it for redundancy: three CDN mirrors, several geolocation strategies, a cache and a network fetch racing where either is acceptable.

Choosing correctly

A few heuristics that resolve most cases:

  • Wrapping Promise.all in try/catch and carrying on anyway? You wanted allSettled.
  • Adding a timeout? race.
  • Have fallbacks and need any one to work? any.
  • Genuinely need all of them and any failure is fatal? all is right.

Edge cases worth knowing

Empty iterables differ per combinator. all([]) and allSettled([]) resolve immediately. any([]) rejects with an empty AggregateError. race([]) hangs forever. That last one is a real bug source when the array is built dynamically.

None of them cancel anything. All four start every promise immediately and leave losers running. Combinators control what you wait for, never what executes.

AggregateError needs a modern target. It landed with Promise.any in ES2021; older browsers need a polyfill for the error type itself, not just the method.

Key takeaways

  • allSettled never rejects — check status on every entry.
  • race settles on the first settlement; any on the first success.
  • race([]) never settles; any([]) rejects immediately.
  • Promises are single-assignment, which is why race needs no bookkeeping.
  • No combinator cancels work — reach for AbortController when losers are expensive.

Goal: Implement all three variants. Focus on the structural difference between them — not just the code.

Frequently asked questions

How do the four Promise combinators differ?
`all` waits for every promise and rejects as soon as one does. `allSettled` waits for every promise and never rejects. `race` settles with whichever promise settles first, fulfilled or rejected. `any` resolves with the first fulfilment and only rejects if every promise rejects. The clean way to remember it: `all` and `allSettled` wait for everything, `race` and `any` want the first — and within each pair one is failure-sensitive and one is not.
Why does allSettled never reject?
Because its whole purpose is reporting outcomes rather than propagating failure. It resolves with an array of `{ status: "fulfilled", value }` or `{ status: "rejected", reason }` objects, one per input in input order. Implementing it means attaching handlers to both paths and recording the result instead of rejecting.
What is special about Promise.any's rejection?
It rejects with an `AggregateError` carrying every individual reason in its `errors` property, and only once the last promise has rejected. That requires a counter — you cannot reject on the first failure, because a later promise may still succeed.
What do all four implementations share?
Preserving input order in the results even though completion order differs, which means writing into `results[i]` by index rather than pushing. Handling the empty-array case, where `all` and `allSettled` resolve immediately and `race` stays pending forever. And accepting non-promise values, since the spec passes everything through `Promise.resolve` first.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

Promise.all Polyfill

Implement your own Promise.all polyfill to understand how concurrent Promise handling works under the hood.

JavaScript · Promises · Async/AwaitPratik Rai ·

JavaScript

Fetch And Retry

Implement a retry function that handles transient failures by retrying async operations with optional delays.

JavaScript · Promises · Async/AwaitPratik Rai ·

JavaScript

Function.prototype.call Polyfill

Implement a Function.prototype.call polyfill from scratch. Master JavaScript this binding, execution context, primitive boxing, and safe property assignment techniques.

JavaScript · Functions · thisPratik Rai ·