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.

Easy to hard promises async coordination error handling

The question

This one is usually phrased as "write your own Promise.all". The interviewer is looking for three specific things:

  1. Does the result array keep input order, not settle order?
  2. Does it handle plain, non-promise values in the input?
  3. 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.

Easy2 cases · values and order

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"]);
Hard2 cases · out-of-order settling

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.

See all 24 questions