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).
JavaScript · Promises · Async — Pratik Rai ·
Implement your own Promise.all polyfill to understand how concurrent Promise handling works under the hood.
Promise.all takes an iterable of promises and returns a single promise that resolves with an array of all their results — or rejects the moment any one of them fails. Reimplementing it is the clearest way to understand promise combinators, and the details that make it tricky are exactly the ones that make concurrent code hard to get right.
The naive mental model is "wait for everything." The real behaviour is more specific: results come back in input order regardless of completion order, rejection is immediate rather than waited-for, and non-promise values pass straight through. Each of those is a design decision with consequences, and writing the polyfill forces you to encode all three.
JSfile.javascript1const results = await Promise.all([ 2 fetch('/a').then((r) => r.json()), 3 fetch('/b').then((r) => r.json()), 4 42, 5]); 6// [aData, bData, 42] — always in this order
/b resolves first, it still lands at index 1. Order comes from the input array, never from timing.[].JSfile.javascript1function promiseAll(iterable) { 2 return new Promise((resolve, reject) => { 3 const items = Array.from(iterable); 4 const results = new Array(items.length); 5 let remaining = items.length; 6 7 if (remaining === 0) { 8 resolve([]); 9 return; 10 } 11 12 items.forEach((item, index) => { 13 Promise.resolve(item).then( 14 (value) => { 15 results[index] = value; 16 remaining--; 17 if (remaining === 0) resolve(results); 18 }, 19 (error) => reject(error) 20 ); 21 }); 22 }); 23}
Three things are load-bearing:
results[index] = value — assigning by the input index rather than pushing is what preserves order. A push-based version returns results in completion order, which is a subtle and nasty bug.
A remaining counter, not results.length — checking the array length fails on sparse arrays and on results that are legitimately undefined. Counting down is unambiguous.
Promise.resolve(item) — wraps plain values and normalises foreign thenables. Calling .then directly on 42 would throw.
The empty-array check must come before the loop, because forEach on an empty array never runs and the promise would hang forever.
This is the most important practical point, and it is not obvious from the API.
When one promise rejects, Promise.all rejects immediately — but the other operations keep running. Promises have no cancellation mechanism. Those fetches complete, those handlers fire, and their results are silently discarded.
That matters when the operations have side effects. Three concurrent writes where the second fails will still leave writes one and three applied. Promise.all gives you no transactionality whatsoever.
If you need real cancellation, you need AbortController:
JSfile.javascript1const controller = new AbortController(); 2try { 3 await Promise.all(urls.map((u) => fetch(u, { signal: controller.signal }))); 4} catch (err) { 5 controller.abort(); // actually stop the others 6 throw err; 7}
Four exist, and picking wrong is common:
| Combinator | Settles when | Rejects when |
|---|---|---|
all | all fulfil | any rejects (immediately) |
allSettled | all settle | never |
race | first settles | if the first to settle rejects |
any | first fulfils | all reject (AggregateError) |
If you are wrapping Promise.all in a try/catch and continuing regardless, you probably wanted allSettled. If you are racing a request against a timeout, you want race. If you are trying several mirrors and need one to work, you want any.
A subtlety worth knowing: with Promise.all, if two promises reject, the second rejection is already "handled" by all internally, so it does not trigger an unhandled rejection warning. But if you build a list of promises and only later pass them to a combinator, rejections that occur in the meantime can fire the warning before anything attaches a handler. Create and combine in the same tick.
"Now implement allSettled." Nearly identical, but never reject — resolve each entry to {status: 'fulfilled', value} or {status: 'rejected', reason}, and only settle when the counter hits zero.
"Implement Promise.all with a concurrency limit." The genuinely useful variant. Promise.all fires everything at once, which will happily open 500 sockets. A pool that keeps N in flight is what production code needs.
"What is the time complexity?" O(n) to set up; wall-clock time is the slowest promise, not the sum — that is the whole point.
Promise.resolve each item so plain values and foreign thenables work.AbortController for real cancellation.Goal: Implement a Promise.all polyfill that handles concurrent Promises, maintains order, and implements fail-fast error handling.
Continue learning with these related challenges
Implement the three Promise utility variants: allSettled (never rejects), race (first to settle wins), and any (first to resolve wins).
JavaScript · Promises · Async — Pratik Rai ·
Implement a retry function that handles transient failures by retrying async operations with optional delays.
JavaScript · Promises · Async/Await — Pratik Rai ·
Run a list of async tasks with a hard limit on how many are in flight at once — the concurrency control Promise.all does not give you.
JavaScript · ES6 — Pratik Rai ·
Implement the three Promise utility variants: allSettled (never rejects), race (first to settle wins), and any (first to resolve wins).
JavaScript · Promises · Async
Pratik Rai ·
Implement a retry function that handles transient failures by retrying async operations with optional delays.
JavaScript · Promises · Async/Await
Pratik Rai ·
Run a list of async tasks with a hard limit on how many are in flight at once — the concurrency control Promise.all does not give you.
JavaScript · ES6
Pratik Rai ·