Promise.all Polyfill
Implement Promise.all the way interviews score it: order-preserving results, fail-fast reject, and thenable wrapping with Promise.resolve.
- javascript
- promises
- polyfill
- async
The usual Promise.all polyfill that fails an interview pushes values as they finish. Input [slow, fast] comes back as [fastValue, slowValue], and the rest of the function is now lying about which slot is which. The second usual fail treats a thenable as a plain object, so { then: (r) => r(1) } lands in the result array uninvoked.
Native Promise.all is fail-fast, order-preserving, and thenable-aware. Empty input fulfills with []. Sparse arrays are not a special case here: arrays are iterable, and the iterator yields undefined for holes, so those slots become fulfilled undefined, not missing entries.
Write a standalone myPromiseAll. Assigning to Promise.all is interview convention and mutates a shared builtin; skip that unless they ask.
Key takeaways
- Results are aligned to input index, not completion order.
- First rejection rejects the result. Later rejections are still handled so they are not unhandled.
Promise.resolvewraps values and thenables. That also forces native microtask reactions.- An empty iterable fulfills with
[]immediately. A non-iterable rejects withTypeError.- Fail-fast does not cancel the other work. Fetches keep running unless you abort them.
Fail-fast, keep order, wrap thenables
Given an iterable of values, Promise.all returns a promise that:
- Fulfills with an array of fulfillment values in iteration order when every input fulfills.
- Rejects with the first rejection reason when any input rejects.
- Treats a non-promise through
Promise.resolve, including thenables. - Fulfills with
[]when the iterable is empty.
function myPromiseAll(iterable) {
return new Promise((resolve, reject) => {
const items = [...iterable];
const n = items.length;
if (n === 0) {
resolve([]);
return;
}
const results = new Array(n);
let remaining = n;
items.forEach((item, i) => {
Promise.resolve(item).then((value) => {
results[i] = value;
remaining -= 1;
if (remaining === 0) resolve(results);
}, reject);
});
});
}
Spreading inside the executor matters. If iterable is not iterable, the constructor sees a thrown TypeError and the returned promise rejects instead of throwing from myPromiseAll itself.
new Array(n) plus assignment at i is the order trick. Completion order only changes when you write the slot, not which slot.
Promise.resolve(item) is the thenable rule. A number, a native promise, and { then: (res) => res('ok') } all become promises you can attach to. Native Promise.resolve also means .then reactions are microtasks even if someone handed you a thenable that calls then synchronously. If you skip the wrap and a thenable invokes its callback now, remaining can hit 0 mid-loop and you resolve a half-filled array.
Why fail-fast still attaches every handler
You start every input. You do not break after the first reject. The outer reject is a no-op the second time, but it is still an onRejected handler, so later failures do not become unhandled rejections.
The other inputs are not cancelled. Promise.all([fetchA(), fetchB()]) rejecting because fetchA 500s does not stop fetchB. If the interviewer asks about production, name AbortController.
const slow = new Promise((res) => setTimeout(() => res('slow'), 50));
const fast = Promise.resolve('fast');
const boom = Promise.reject(new Error('nope'));
myPromiseAll([slow, fast]).then(console.log); // ['slow', 'fast']
myPromiseAll([slow, boom]).catch((err) => console.log(err.message)); // 'nope'
myPromiseAll([]); // fulfills with []
Footguns in a Promise.all polyfill
| Case | Spec behavior | Broken polyfill |
|---|---|---|
| Empty iterable | Fulfill [] |
Hang, or reject |
| Thenable | Adopt then |
Store the object |
| Completion order | Index-aligned | push as they finish |
| Hole in an array | Iterator yields undefined |
Skip the index and shift later values |
| String | Iterable of characters | Accidental Promise.all('abc') → ['a','b','c'] |
| First reject | Reject, keep others running | return from the loop and leave later rejects unhandled |
Strings are iterable. That is legal and surprising. Sets dedupe by value, so Promise.all(new Set([p, p])) is one slot.
Do not implement all in terms of async/await in a for loop. That is series, not parallel. The series and parallel pages are the orchestration follow-on. Here, every Promise.resolve(item).then must be registered before you wait.
remaining starts at n, not 0. If you increment as you go and check remaining === 0 only inside then, an empty list never calls then and hangs. That hang is correct for race([]), not for all([]).
A standalone function is enough. If they insist on Promise.myAll = myPromiseAll, say it mutates a shared builtin and can clash with other polyfills.
Interview answer (out loud)
“Promise.all takes an iterable, wraps each item with Promise.resolve, and fulfills with values in input order. Empty input fulfills with an empty array. The first rejection rejects the result; I still attach reject handlers to every item so later failures are not unhandled. Fail-fast does not cancel the rest. I would not push into the results array as promises finish, and I would not skip Promise.resolve, because thenables and sync thenables both break that shortcut.”
Further reading
Related on this site
- Promise.allSettled polyfill
- Promise.race polyfill
- Promise.any polyfill
- Promises
- The event loop
- Polyfill questions playbook
- JavaScript interview learning path
Related guides
- Custom Promise From ScratchBuild a tiny Promise: executor, then chaining, microtask reactions, resolve-thenable unwrapping, and the states you are not allowed to leave.
- Promise.allSettled PolyfillWrite Promise.allSettled as a status bag, not a silent all. Fulfilled and rejected entries, order, and why it never rejects for input failures.
- Promise.any PolyfillImplement Promise.any: first fulfillment wins, all-reject becomes AggregateError. Opposite of all, not a synonym for race.
- Promise.finally PolyfillPolyfill Promise.prototype.finally: run cleanup on fulfill and reject, pass the original value through, and wait if the callback returns a thenable.
- Promise.race PolyfillImplement Promise.race: first settlement wins, including a fast rejection. Timeouts, cancellation myths, and the empty-iterable hang.