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.
- 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 asall.- Wrap with
Promise.resolveso 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 on this site
- Promise.all polyfill
- Promise.any polyfill
- Promises
- The event loop
- Polyfill questions playbook
- Run async tasks in parallel
Related guides
- Custom Promise From ScratchBuild a tiny Promise: executor, then chaining, microtask reactions, resolve-thenable unwrapping, and the states you are not allowed to leave.
- Promise.all PolyfillImplement Promise.all the way interviews score it: order-preserving results, fail-fast reject, and thenable wrapping with Promise.resolve.
- Promise.any PolyfillImplement Promise.any: first fulfillment wins, all-reject becomes AggregateError. Opposite of all, not a synonym for race.
- Promise.finally PolyfillPolyfill Promise.prototype.finally: run cleanup on fulfill and reject, pass the original value through, and wait if the callback returns a thenable.
- Promise.race PolyfillImplement Promise.race: first settlement wins, including a fast rejection. Timeouts, cancellation myths, and the empty-iterable hang.