Back to JavaScript
JavaScript
easy
mid

How would you write a polyfill for Promise.all that handles 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).

3 min read·~15 min to think through

Implementation

js
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.from normalizes.
  • Empty input resolves immediately with [].
  • Non-Promise values are accepted; Promise.resolve(p) wraps them (so promiseAll([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

js
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

js
function promiseAllSettled(iterable) {
  return promiseAll(
    Array.from(iterable).map((p) =>
      Promise.resolve(p).then(
        (value) => ({ status: "fulfilled", value }),
        (reason) => ({ status: "rejected", reason }),
      ),
    ),
  );
}

race

js
function promiseRace(iterable) {
  return new Promise((resolve, reject) => {
    for (const p of iterable) Promise.resolve(p).then(resolve, reject);
  });
}

any

js
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.from does 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.

Senior engineer discussion

Seniors knock out all four variants quickly and discuss the edge cases (empty, non-Promise, mutation) before being asked.

Related questions