intermediateVery common5 min read · Updated Jul 18, 2026

Implement Promise.all from Scratch

Implement Promise.all in JavaScript: order preservation with a counter, fail-fast rejection, iterable inputs, non-promise values, and the empty-array case.


The problem

Implement promiseAll(iterable): resolve with all results in input order once every promise fulfills; reject immediately with the first rejection.

This is the most-asked combinator implementation, and it's really a test of three specific insights:

  1. Order ≠ completion order. Results must land at the input index even though promises finish in any order.
  2. You can't use results.length to detect completion. Assigning results[5] on an empty array makes length 6 with holes — you need a fulfillment counter.
  3. Fail-fast is free. Calling reject on the first rejection wins the settle-once race; later fulfillments call resolve into a dead promise harmlessly.

Where you see it in production

Parallel independent fetches on page load (Promise.all([fetchUser(), fetchCart(), fetchFlags()])), batched writes, and asset preloading. The senior framing: Promise.all is for all-or-nothing dependencies — if the page can render with partial data, allSettled is the right tool, and if the input list is large you need a concurrency pool instead of firing everything at once.

Implementation

function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable); // accept ANY iterable, like the spec
    const results = new Array(items.length);
    let remaining = items.length;
 
    if (remaining === 0) {
      resolve([]); // empty input resolves immediately — a classic hang bug
      return;
    }
 
    items.forEach((item, index) => {
      // Promise.resolve handles plain values and foreign thenables alike
      Promise.resolve(item).then(
        (value) => {
          results[index] = value; // input order, not completion order
          remaining -= 1;
          if (remaining === 0) resolve(results);
        },
        reject // first rejection wins; settle-once ignores the rest
      );
    });
  });
}

Dry run

const slow = new Promise((res) => setTimeout(() => res("slow"), 300));
const fast = new Promise((res) => setTimeout(() => res("fast"), 100));
 
await promiseAll([slow, fast, 42]);
// t=0:   remaining = 3; all three subscriptions attached
// t≈0:   42 (wrapped by Promise.resolve) fulfills → results[2] = 42, remaining = 2
// t=100: fast → results[1] = "fast", remaining = 1
// t=300: slow → results[0] = "slow", remaining = 0 → resolve
// → ["slow", "fast", 42]  ← input order, even though 42 finished first

And the fail-fast path:

promiseAll([
  new Promise((res) => setTimeout(res, 500, "never seen")),
  Promise.reject(new Error("boom")),
]).catch((e) => console.log(e.message)); // "boom" — no waiting for the 500ms one

Note what fail-fast does not do: the slow promise keeps runningPromise.all stops waiting, it doesn't cancel. If the interviewer asks how you'd actually stop the work, the answer is AbortController.

Edge cases interviewers probe

  • Empty input → resolve [] synchronously-ish. Forgetting this hangs forever: the forEach never runs, resolve is never called.
  • Non-promise valuespromiseAll([1, 2, 3])[1, 2, 3]. Promise.resolve per item handles it.
  • Any iterable, not just arrays — the real Promise.all accepts Sets, generators, strings. Array.from up front matches the spec; a hard Array.isArray check that rejects iterables is a spec deviation (worth naming, since many "polyfills" have it).
  • Multiple rejections — only the first matters; the rest hit an already-settled promise. No unhandled-rejection noise either, because each item's rejection was handled — by your reject.
  • Duplicate promises in the inputpromiseAll([p, p]) gives the value twice at both indexes; nothing special needed, but interviewers use it to check the counter isn't keyed by promise identity.

Common mistakes

  • results.push(value) — completion order corrupts the output. The planted bug.
  • if (results.length === items.length) — sparse arrays make length lie; use a counter.
  • Missing empty-input check — the eternal hang.
  • .then(...).catch(reject) instead of the two-argument then — functionally similar here, but the two-arg form states intent and avoids catching errors thrown by your own fulfillment handler (a subtle self-inflicted bug: if results[index] = value code throws, .catch(reject) would reject the outer promise while the two-arg form lets it surface).
  • Claiming Promise.all "runs promises in parallel" — promises are already running when you receive them; all only aggregates. The distinction matters and interviewers listen for it (see N tasks in parallel).

Test yourself

Predict the outputWhat prints, and when?
Promise.all([
Promise.resolve(1),
new Promise((res) => setTimeout(res, 100, 2)),
Promise.reject(new Error("x")),
]).then((v) => console.log("ok", v))
.catch((e) => console.log("err", e.message));

The combinator family

Resolves whenRejects whenEmpty input
Promise.allevery promise fulfillsfirst rejectionresolves []
allSettledevery promise settlesneverresolves []
anyfirst fulfillmentall reject (AggregateError)rejects
racefirst settle (either way)first settle (either way)pends forever

Being able to reproduce this table — especially the empty-input column — is a better interview signal than any single implementation.

Follow-up questions

  • "Now make it never reject." → implement allSettled; it's a 5-line delta from what you wrote.
  • "1,000 URLs but max 5 in flight?" → the promise pool — the natural senior escalation of this question.
  • "How would you add a timeout?"Promise.race with a timer: race + timeout pattern.
  • "Why does the counter approach not need locks?" — JavaScript is single-threaded; the remaining -= 1 in different then callbacks can never interleave mid-operation. See the event loop.
  • "What happens to the other promises after a rejection?" — they run to completion, results discarded; cancellation needs AbortController.

  • Promise.allSettled

    intermediate

    Implement allSettled: wait for every promise and report per-promise status objects.

  • Promise.any

    intermediate

    Implement Promise.any with AggregateError semantics: first success wins, all failures reject.

  • Promise.race

    beginner

    Implement Promise.race and use it for the timeout pattern every senior interview touches.