ESC

Type to search the knowledge base.

Promise.allSettled Polyfill

Write Promise.allSettled as a status bag, not a silent all. Fulfilled and rejected entries, order, and why it never rejects for input failures.

intermediate5 min read
  • javascript
  • promises
  • polyfill
  • async

A dashboard that uses Promise.all for user, feed, and ads will blank the page when ads 500. The interviewer then asks for Promise.allSettled, and a common fake is Promise.all(items.map((p) => p.catch(() => undefined))). That swallows the reason, collapses fulfill and “I failed” into the same undefined, and teaches the next function to guess.

allSettled waits for every input to settle and fulfills with a status bag per index. It does not reject because an input rejected. Empty input fulfills with []. Thenables still go through Promise.resolve.

Historically all is ES2015 and allSettled is ES2020, so a polyfill that builds on native Promise.all is honest. You can also write it from counters if they forbid using all. Prefer a standalone myPromiseAllSettled. Do not assign to Promise.allSettled unless they ask; that mutates a shared builtin and can hide the native method in tests.

Key takeaways

  • Every input becomes { status: 'fulfilled', value } or { status: 'rejected', reason } at the same index.
  • Input rejections do not reject the outer promise. Iterator failure still can.
  • Empty iterable fulfills with [], same shape as all.
  • Wrap with Promise.resolve so thenables and plain values share one path.
  • This is fail-soft data, not cancellation, and not a substitute for logging the reason.

A result for every input, even the broken ones

Contrast with all: you still wait for the whole set, and you still keep order. You change the payload. Fulfillments and rejections both become objects you can branch on.

function myPromiseAllSettled(iterable) {
  const wrapped = [...iterable].map((item) =>
    Promise.resolve(item).then(
      (value) => ({ status: 'fulfilled', value }),
      (reason) => ({ status: 'rejected', reason }),
    ),
  );
  return Promise.all(wrapped);
}

Each mapped promise fulfills with a record. Promise.all therefore never sees a rejection from the inputs. If spreading the iterable throws, do it inside new Promise if you want a rejected promise instead of a sync throw:

function myPromiseAllSettled(iterable) {
  return new Promise((resolve, reject) => {
    let items;
    try {
      items = [...iterable];
    } catch (err) {
      reject(err);
      return;
    }

    const n = items.length;
    if (n === 0) {
      resolve([]);
      return;
    }

    const results = new Array(n);
    let remaining = n;

    items.forEach((item, i) => {
      Promise.resolve(item).then(
        (value) => {
          results[i] = { status: 'fulfilled', value };
        },
        (reason) => {
          results[i] = { status: 'rejected', reason };
        },
      ).then(() => {
        remaining -= 1;
        if (remaining === 0) resolve(results);
      });
    });
  });
}

The inner .then after both branches is the “always count this slot” step. Do not decrement only in the success branch.

Use the strings 'fulfilled' and 'rejected' exactly. Discriminate with status, not with 'value' in record, because a fulfilled undefined is legal.

const results = await myPromiseAllSettled([
  Promise.resolve(1),
  Promise.reject(new Error('nope')),
  3,
  { then: (res) => res(4) },
]);

// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: Error('nope') },
//   { status: 'fulfilled', value: 3 },
//   { status: 'fulfilled', value: 4 },
// ]

await myPromiseAllSettled([]); // []

A thenable that rejects (its then calls the second callback, or throws inside the thenable job) becomes a 'rejected' row. It does not reject allSettled.

Input settlement Record
Fulfill v { status: 'fulfilled', value: v }
Reject e { status: 'rejected', reason: e }
Thenable that fulfills Fulfilled record
Empty iterable [] (fulfilled)

One input that never settles means allSettled never settles. Same as all. Fail-soft is not “return what you have after N ms.” If the interviewer wants a timeout per widget, wrap each item before allSettled, and still abort IO if the product cares.

The status strings are lowercase 'fulfilled' and 'rejected'. A polyfill that writes 'success' / 'error' will break switch statements copied from MDN. Keep value only on fulfilled records and reason only on rejected ones. Mixing both keys on every object makes in checks lie.

Footguns in a Promise.allSettled polyfill

p.catch(() => undefined) is not allSettled. You lost reason, and you cannot tell a successful undefined from a failure. p.catch((reason) => reason) is also wrong: a fulfilled error object and a rejected error object look the same.

Do not push records as they settle. Ads finishing first must not become results[0] if ads were input [2].

Empty iterable: fulfill []. If you reuse a race brain and leave the promise pending, widget-load code that awaits it hangs the UI.

allSettled does not reject for input failures. It can reject if the iterable is not iterable, or if constructing the result throws. Say that if they ask “never rejects.”

It does not cancel anything. A rejected fetch still hit the network. Fail-soft rendering is a UI policy; abort is a separate policy. For a timeout that should actually stop IO, see timeout a promise and AbortController, not this combinator.

Do not recast allSettled as “all plus a default.” The status bag is the API. Logging reason on rejected rows is how you still notice a 500 while the page stays up. Callers should look like:

const [user, feed] = await myPromiseAllSettled([loadUser(), loadFeed()]);
render({
  user: user.status === 'fulfilled' ? user.value : guest,
  feed: feed.status === 'fulfilled' ? feed.value : [],
});

Interview answer (out loud)

“Promise.allSettled waits for every input to settle and fulfills with a status object per index, fulfilled or rejected. It does not reject just because an input rejected. Empty input fulfills with an empty array. I wrap each item with Promise.resolve so thenables participate, and I write results by index, not push. A catch that returns undefined is not this method: it drops the reason and the discriminant.”

Further reading

Related guides