Implement Promise.all
Promise.all takes an iterable of promises and returns one promise that fulfils with an array of their values, in input order, or rejects with the first rejection. Track a remaining counter, and write each value into results[index] rather than pushing, because promises settle in whatever order they finish.
The question
This one is usually phrased as "write your own Promise.all". The interviewer is
looking for three specific things:
- Does the result array keep input order, not settle order?
- Does it handle plain, non-promise values in the input?
- Does it reject as soon as any input rejects, rather than waiting for the rest?
The solution
Almost all of the difficulty is in the indexing. The counter, not the length of the results array, is what tells you when everything has settled.
function promiseAll(iterable) {
return new Promise((resolve, reject) => {
const items = Array.from(iterable);
const results = new Array(items.length);
let remaining = items.length;
if (remaining === 0) {
resolve(results);
return;
}
items.forEach((item, index) => {
// Promise.resolve normalises plain values, thenables and promises alike.
Promise.resolve(item).then((value) => {
// Assign by index, never push: settle order is not input order.
results[index] = value;
remaining -= 1;
if (remaining === 0) resolve(results);
}, reject);
});
});
}
How it works
Assign by index, never push. Pushing gives you settle order, and settle order is only input
order by coincidence. A separate counter decides when everything is done, because checking
results.length doesn’t work: assigning to index 2 first makes length jump to 3
while positions 0 and 1 are still empty.
Promise.resolve flattens the input, turning plain values and thenables into
promises so one code path covers all three. Rejection needs nothing special. Pass
reject straight through and the first failure settles the outer promise; later
ones are ignored, because a promise settles once. Empty input resolves immediately with an
empty array, which is easy to forget and is what the real method does.
The tests it has to pass
This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 4 cases Practice Pad runs against your solution, and the implementation above passes all of them.
✓ resolves plain values and promises alike, in input order
const result = await promiseAll([1, Promise.resolve(2), 3]);
expectEqual(result, [1, 2, 3]);
✓ resolves an array of already-resolved promises
expectEqual(await promiseAll([Promise.resolve("a"), Promise.resolve("b")]), ["a", "b"]);
✓ keeps input order when later promises settle first
const slow = new Promise((resolve) => setTimeout(() => { resolve("slow"); }, 30));
const fast = Promise.resolve("fast");
expectEqual(await promiseAll([slow, fast]), ["slow", "fast"]);
✓ preserves order across promises that settle in reverse
const at = (ms, value) => new Promise((resolve) => setTimeout(() => { resolve(value); }, ms));
const result = await promiseAll([at(60, "slow"), at(30, "middle"), at(5, "fast")]);
expectEqual(result, ["slow", "middle", "fast"]);
Edge cases to watch out for
Pushing results as they arrive
The results then come back in the order the promises settled, which is only the input order by coincidence. Both hard tests exist to catch this, using promises that deliberately finish in reverse.
Using results.length to decide when to resolve
Assigning results[2] first makes length jump to 3 while positions 0 and 1 are still empty, so the promise resolves early with holes in it. Count separately.
Forgetting the empty-iterable case
With no items the counter never decrements, so a naive implementation hangs forever instead of resolving with [].
Follow up questions
How is Promise.all different from Promise.allSettled?
Promise.all rejects as soon as any input rejects, and you lose the values from the ones that succeeded. Promise.allSettled always fulfils, with an array of { status, value } or { status, reason } objects, so you can inspect every outcome. Use allSettled when partial success is meaningful.
Does Promise.all run promises in parallel?
It doesn’t run anything. The promises passed in have already started, because a promise begins executing the moment it’s created. Promise.all only coordinates their completion. If you need to control concurrency you’ve to avoid creating them all up front.
How would you add a timeout to Promise.all?
Race the whole thing: Promise.race([promiseAll(items), rejectAfter(ms)]). Note that the underlying promises keep running, since a promise can’t be cancelled, so the timeout stops you waiting rather than stopping the work. To stop the work you need an AbortController threaded into each operation.
What happens to the other promises when one rejects?
Nothing stops them. They carry on running and their results are discarded, because the outer promise has already settled and a promise can only settle once. There’s no cancellation in the promise model, which is why AbortController exists separately.
Practise this with the tests running
Practice Pad runs these 4 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.
Download for macOS Free. Requires macOS 12 or later on Apple Silicon.