Write a polyfill for Promise.all handling both resolve and reject cases.
Return a new Promise. Track a results array and a fulfilled count. For each input promise, on fulfill: write the value at its index, increment count, resolve outer when count === length. On reject: reject the outer Promise immediately (short-circuit). Handle the empty-array case (resolve with []) and non-Promise inputs (wrap with Promise.resolve).
Implementation
function promiseAll(iterable) {
return new Promise((resolve, reject) => {
const arr = Array.from(iterable);
const results = new Array(arr.length);
let remaining = arr.length;
if (remaining === 0) return resolve(results);
arr.forEach((p, i) => {
Promise.resolve(p).then(
(value) => {
results[i] = value;
if (--remaining === 0) resolve(results);
},
(reason) => reject(reason),
);
});
});
}Walking through key details
- Iterable, not array — the real spec accepts any iterable;
Array.fromnormalizes. - Empty input resolves immediately with
[]. - Non-Promise values are accepted;
Promise.resolve(p)wraps them (sopromiseAll([1, 2, fetch(...)])works). - Result order matches input order, not completion order — that's why we write to
results[i]. - First rejection short-circuits — outer reject is called immediately; subsequent rejections are ignored (Promise settles once).
- No await / no async — pure Promise composition.
Test cases
await promiseAll([1, 2, 3]); // [1, 2, 3]
await promiseAll([]); // []
await promiseAll([Promise.resolve("a"), "b"]); // ["a", "b"]
await promiseAll([fetch("/x"), fetch("/y")]); // [Response, Response]
try { await promiseAll([Promise.reject(new Error("x"))]); } catch (e) { /* x */ }Variants worth knowing
allSettled
function promiseAllSettled(iterable) {
return promiseAll(
Array.from(iterable).map((p) =>
Promise.resolve(p).then(
(value) => ({ status: "fulfilled", value }),
(reason) => ({ status: "rejected", reason }),
),
),
);
}race
function promiseRace(iterable) {
return new Promise((resolve, reject) => {
for (const p of iterable) Promise.resolve(p).then(resolve, reject);
});
}any
function promiseAny(iterable) {
return new Promise((resolve, reject) => {
const arr = Array.from(iterable);
const errors = new Array(arr.length);
let remaining = arr.length;
if (remaining === 0) return reject(new AggregateError([], "All promises rejected"));
arr.forEach((p, i) => {
Promise.resolve(p).then(resolve, (e) => {
errors[i] = e;
if (--remaining === 0) reject(new AggregateError(errors, "All promises rejected"));
});
});
});
}Edge cases interviewers probe
- Empty input →
Promise.all([])resolves with[]. - Non-Promise inputs → must work.
- Multiple rejections → only the first matters.
- Mutation of input array mid-run → spec says it's snapshotted at iteration time; our
Array.fromdoes that.
Interview framing
"Return a new Promise. Build a results array sized to the input. For each input, wrap with Promise.resolve so non-Promise values work, then attach a then with two handlers: success writes the value at index and decrements a counter (resolve when 0); failure rejects the outer immediately. Handle empty iterable up front. The variants — allSettled, race, any — all reuse the same shape with different settle logic."
Follow-up questions
- •How does Promise.allSettled differ?
- •What is AggregateError?
- •What if the input is an async iterable?
Common mistakes
- •Pushing to results instead of indexing → wrong order.
- •Forgetting Promise.resolve wrap → fails for non-Promise inputs.
- •Forgetting the empty case → outer never resolves.
Performance considerations
- •O(n) wiring. Microtask floods possible if N is huge.
Edge cases
- •Empty iterable.
- •Mixed Promise + non-Promise.
- •Inputs that resolve synchronously.
Real-world examples
- •Used as interview question across most senior loops. Bluebird, Q, p-each-series wrap similar primitives.