InterviewsVector
intermediateCommon4 min read · Updated Jul 18, 2026

Implement Promise.allSettled from Scratch

Implement Promise.allSettled in JavaScript: status objects for every outcome, why it never rejects, and the reflect-pattern one-liner built on Promise.all.


The problem

Implement allSettled(iterable): wait for every promise to settle and resolve with an array of { status: "fulfilled", value } / { status: "rejected", reason } objects, in input order. The returned promise never rejects.

Usually asked as the follow-up to Promise.all — and the smart move is to say it's a delta, then show the delta: same counter, same index-addressed results, but rejections are captured as data instead of propagated.

Where you see it in production

Anywhere partial failure is acceptable: a dashboard loading 6 widgets where 5 should render if one API is down; batch jobs reporting per-item outcomes; fire-N-notifications-and-log-failures. The design sentence interviewers reward: all treats rejection as control flow; allSettled treats it as data.

Implementation

function allSettled(iterable) {
  return new Promise((resolve) => {   // note: no reject parameter needed
    const items = Array.from(iterable);
    const results = new Array(items.length);
    let remaining = items.length;
 
    if (remaining === 0) {
      resolve([]);
      return;
    }
 
    items.forEach((item, index) => {
      Promise.resolve(item).then(
        (value)  => { results[index] = { status: "fulfilled", value }; },
        (reason) => { results[index] = { status: "rejected",  reason }; }
      ).then(() => {
        remaining -= 1;              // one counter decrement for either outcome
        if (remaining === 0) resolve(results);
      });
    });
  });
}

The chained second .then runs after either branch recorded its result — one decrement site instead of two copies, which is exactly the kind of duplication interviewers nudge you to factor out.

The one-liner version (the "reflect" pattern)

If you've already implemented Promise.all, allSettled is a mapping:

const reflect = (p) =>
  Promise.resolve(p).then(
    (value)  => ({ status: "fulfilled", value }),
    (reason) => ({ status: "rejected",  reason })
  );
 
const allSettled = (items) => Promise.all(Array.from(items).map(reflect));

reflect converts any promise into one that always fulfills — with its outcome as data. Once nothing can reject, Promise.all's fail-fast has nothing to fail on. Offering both versions — "here's the from-scratch one; here's how I'd actually write it" — is a strong senior move.

Verified behavior

await allSettled([
  Promise.resolve(1),
  Promise.reject(new Error("nope")),
  "plain value",
]);
// [
//   { status: "fulfilled", value: 1 },
//   { status: "rejected",  reason: Error("nope") },
//   { status: "fulfilled", value: "plain value" },
// ]
// — and the outer promise FULFILLED despite the rejection in the middle

Edge cases interviewers probe

  • Exact result shapevalue key on fulfillment, reason key on rejection, never both. Downstream code does r.status === "fulfilled" ? r.value : fallback; wrong keys break it silently.
  • Empty input[], same as all.
  • Never rejects — so what about errors in your code? If the mapping callback itself threw, the executor would reject the outer promise. Keeping the per-item handlers trivial is the defense; worth saying aloud.
  • Order — input order, regardless of settle order; same index-addressing argument as Promise.all.
  • Non-promise values — wrapped, reported as fulfilled.

Common mistakes

  • Rejecting the outer promise on first rejection — that's Promise.all; the entire point here is that rejection is data.
  • Two separate remaining decrements drifting apart during edits (the factored .then above avoids it).
  • { status, value, reason } with undefined filler keys — close, but observably different from spec via "reason" in result.
  • Using .finally for the decrement but recording results in earlier links sloppily — fine when careful, but the two-arg then + chained decrement is harder to get wrong.
  • Inventing extra fields (timestamps, indexes) when asked for the spec method — save extensions for after the spec shape is correct, and label them as extensions.

The combinator family

Resolves whenRejects whenEmpty input
allevery fulfillsfirst rejectionresolves []
allSettledevery settlesneverresolves []
anyfirst fulfillmentall rejectrejects
racefirst settlefirst settlepends forever

Follow-up questions

  • "When is allSettled the wrong choice?" — when downstream work needs every result: silently rendering with holes can be worse than failing loudly. Partial tolerance is a product decision, not a default.
  • "How would you type the result in TypeScript?" — a discriminated union on status: { status: "fulfilled"; value: T } | { status: "rejected"; reason: unknown } — narrowing via if (r.status === "fulfilled") is the idiomatic consumer pattern.
  • "Add a concurrency limit." — combine the reflect pattern with a promise pool.
  • "Log every failure but return only successes." — a filter+map over the settled array; interviewers use it to check you can consume the shape you just built.
  • "Which came first, this or userland?" — userland: the reflect pattern predates ES2020; allSettled standardized what everyone was hand-rolling. Knowing the direction of that flow is good historical taste.

  • Promise.any

    intermediate

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

  • Promise.all

    intermediate

    Implement Promise.all: aggregate results in order, fail fast on the first rejection.

  • Promise.race

    beginner

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