#Promises#Polyfill

Promise.all Polyfill

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

By Pratik RaiMedium

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.

Why implement it yourself

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.

What it guarantees

JSfile.javascript
1const 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
  • Order is preserved. If /b resolves first, it still lands at index 1. Order comes from the input array, never from timing.
  • Rejection is immediate. The first failure rejects the whole thing. It does not wait for the others.
  • Non-promises are allowed. Any non-thenable is wrapped and resolved as-is.
  • An empty array resolves immediately with [].

The implementation

JSfile.javascript
1function 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.

Rejection does not mean cancellation

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.javascript
1const 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}

Choosing the right combinator

Four exist, and picking wrong is common:

CombinatorSettles whenRejects when
allall fulfilany rejects (immediately)
allSettledall settlenever
racefirst settlesif the first to settle rejects
anyfirst fulfilsall 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.

Unhandled rejections

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.

Common follow-ups

"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.

Key takeaways

  • Results are ordered by input position, never by completion time.
  • Track completion with a counter, not the result array's length.
  • Promise.resolve each item so plain values and foreign thenables work.
  • Handle the empty iterable before iterating, or the promise never settles.
  • Rejection stops waiting, not the work — use AbortController for real cancellation.

Goal: Implement a Promise.all polyfill that handles concurrent Promises, maintains order, and implements fail-fast error handling.

Frequently asked questions

Why do interviewers ask for a Promise.all polyfill?
Because it cannot be written correctly without understanding that promises settle in an unpredictable order while the results must stay in the original one. It also checks whether you know the failure behaviour, which is where most answers go wrong.
How does Promise.all preserve the order of results?
By writing each result into the index it started from rather than pushing as things resolve. Pushing produces completion order, which looks right whenever the promises happen to finish in sequence and silently breaks as soon as they do not — a bug that passes a casual test.
What happens when one promise rejects?
The returned promise rejects immediately with that first error, and the remaining promises are not cancelled — they keep running, their results are simply ignored. Saying that out loud is worth marks, because it shows you know a rejected `Promise.all` does not stop the work already in flight.
How is Promise.allSettled different?
`allSettled` never rejects. It waits for every promise to finish and resolves with a status and value or reason for each, which is what you want when you need all the outcomes rather than an early exit. `Promise.all` is the right choice only when any single failure makes the whole result useless.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

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 · AsyncPratik 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

Promise Pool

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 · ES6Pratik Rai ·