Implement a Promise.allSettled polyfill
Map each input to a promise that resolves with {status:'fulfilled', value} on success or {status:'rejected', reason} on failure. Pass the wrapped array to Promise.all so the outer promise never rejects.
Promise.allSettled waits for every input promise to either resolve or reject, and resolves with an array of { status, value } or { status, reason } objects. Unlike Promise.all, it never rejects — partial failure is reported, not thrown. Useful for "fan out N independent requests; show whatever succeeded."
The trick. Wrap each input promise so its rejection becomes a fulfilled wrapper object. Then a plain Promise.all over the wrappers does the rest.
type Settled<T> =
| { status: "fulfilled"; value: T }
| { status: "rejected"; reason: unknown };
function allSettled<T>(promises: Iterable<T | PromiseLike<T>>): Promise<Settled<T>[]> {
return Promise.all(
Array.from(promises, p =>
Promise.resolve(p).then(
value => ({ status: "fulfilled" as const, value }),
reason => ({ status: "rejected" as const, reason })
)
)
);
}Why this works. Each then returns a new promise. The second argument (onRejected) catches failure and maps it to a fulfilled wrapper, so the outer Promise.all never sees a rejection.
Empty input. Promise.allSettled([]) resolves with [] immediately — Promise.all([]) does the same, so this comes for free.
Iterable, not just array. The spec accepts any iterable. Array.from(promises, fn) handles both arrays and iterables (including generators).
Why Promise.resolve(p)? Inputs may be plain values (42) or thenables (objects with a then method that aren't real promises). Promise.resolve normalizes everything into a real promise.
Sibling polyfills (interviewers love a follow-up).
Promise.any— resolves with the first fulfilled value; rejects withAggregateErroronly if all reject. Inverse ofallSettledfor happy path.Promise.race— resolves/rejects with whichever settles first, including the first rejection. Common foot-gun: a single fast rejection drops every other in-flight result.Promise.all— fail-fast. First rejection rejects the whole.
Common interview tweaks.
- "Without using Promise.all" → loop with a counter, push results, resolve when
completed === total. Watch the off-by-one. - "Limit concurrency to N" → asks for a pool/semaphore on top of allSettled.
When to actually use it. Dashboards (each widget fetches independently; show what loaded), batch validation (validate N forms, report all errors), search across multiple providers (use whatever returns).
Code
Follow-up questions
- •Implement Promise.any — what does AggregateError look like?
- •How would you add a concurrency limit (e.g., max 5 in-flight)?
- •Why does Promise.race never resolve when given an empty array?
- •Implement Promise.all from scratch.
Common mistakes
- •Using catch instead of the second then arg — works but less explicit.
- •Not normalizing inputs with Promise.resolve — breaks for non-promise values.
- •Treating the result as an array of values — it's wrappers; you must read .value or .reason.
Performance considerations
- •Memory: results array holds N entries until all settle — for huge N use streaming.
- •Microtask ordering: each wrapper adds one tick — usually negligible.
Edge cases
- •Empty iterable resolves with [] in the same microtask.
- •A rejected reason that is itself a promise is NOT awaited — it's stored as-is.
- •If a thenable's then throws synchronously, treat as rejected.
Real-world examples
- •Dashboards that aggregate independent API calls (revenue, users, alerts) and render whatever succeeded.