Implement Promise.all, Promise.allSettled, and Promise.race
The Short Answer
Promise.all, Promise.allSettled, and Promise.race are static combinators that take an iterable of promises and return a single new promise. They differ in when that promise settles and what it settles with: all waits for every promise but fails fast on the first rejection, allSettled waits for every promise and never rejects, and race settles as soon as the first promise settles either way.
Implementing them is a great test of how well you can coordinate multiple async results with a counter and an index. The snippet below shows the behaviour we are reproducing so the differences between the three are concrete before we write any code.
const tasks = [Promise.resolve(1), Promise.resolve(2), Promise.reject("x")];
Promise.all(tasks).catch((err) => console.log(err)); // "x" — fails fast
Promise.allSettled(tasks).then((r) => console.log(r)); // array of {status,...}
Promise.race(tasks).then((v) => console.log(v)); // 1 — first to settle
How They Differ
All three accept the same input and return a single promise — the distinction is purely in their settling rules. This table is worth committing to memory, because interviewers often ask you to compare them before writing any code.
| Method | Resolves when | Rejects when |
|---|---|---|
| all | Every promise fulfils | Any one rejects (fail fast) |
| allSettled | Every promise settles | Never — always resolves |
| race | First promise settles (fulfilled) | First promise settles (rejected) |
Implementing Promise.all
The challenge with all is preserving result order even though promises settle at different times. We solve it by writing each result into the array at its original index and tracking a counter of how many have completed. We resolve only when the counter hits the total, and reject immediately on the first failure. Watch the index capture inside the loop — that is what keeps results ordered.
function myAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
const items = [...promises];
if (items.length === 0) return resolve(results); // empty resolves now
items.forEach((promise, index) => {
// wrap with resolve so plain (non-promise) values work too
Promise.resolve(promise).then(
(value) => {
results[index] = value; // preserve original order
completed += 1;
if (completed === items.length) resolve(results);
},
(reason) => reject(reason) // first rejection wins
);
});
});
}
Why wrap with Promise.resolve
The input iterable may contain plain values, not just promises. Promise.resolve(promise) normalises everything to a promise so .then is always safe to call — this matches native behaviour exactly.
Implementing Promise.allSettled
allSettled never rejects — it reports the outcome of every promise instead. So both the success and failure branches write a descriptive object into the results array and bump the same counter. The shape of those objects ({ status: 'fulfilled', value } or { status: 'rejected', reason }) matches the spec, which interviewers like to see you reproduce precisely.
function myAllSettled(promises) {
return new Promise((resolve) => {
const results = [];
let completed = 0;
const items = [...promises];
if (items.length === 0) return resolve(results);
items.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = { status: "fulfilled", value };
completed += 1;
if (completed === items.length) resolve(results);
},
(reason) => {
results[index] = { status: "rejected", reason };
completed += 1;
if (completed === items.length) resolve(results);
}
);
});
});
}
Notice there is no reject call anywhere in this implementation — that is the defining feature of allSettled. A rejected input promise simply produces a { status: 'rejected', reason } entry instead of tearing down the whole operation, which is why it is the right choice when you want every result regardless of individual failures.
Implementing Promise.race
race is the simplest of the three. Because a promise can only settle once, we can attach resolve and reject directly to every input promise and let the first one to finish win — all later settlements are ignored automatically by the one-way nature of promises. No counter is needed.
function myRace(promises) {
return new Promise((resolve, reject) => {
// whichever settles first wins; the rest are ignored
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject);
}
});
}
Why no guard is needed
The returned promise can only transition once. So even though we call resolve/reject for every input, only the first call has any effect — the combinator's own state machine enforces the race for us.
Common Mistakes
Pushing results instead of indexing them
Using results.push(value) records them in completion order, not input order, so Promise.all returns a scrambled array when promises settle at different speeds.
✅Write to results[index] using the captured loop index to preserve order.
Forgetting the empty-iterable case
With no input promises the completion counter never reaches the length, so the returned promise hangs forever in pending.
✅Resolve immediately with an empty array when the input has zero items.
Why Interviewers Ask This
These combinators show up constantly in real code — running requests in parallel, waiting for several resources, or timing out the slowest call. Implementing them proves you can coordinate concurrent async work, preserve ordering, and reason about settling semantics. Being able to explain why allSettled never rejects and why race needs no counter signals genuine command of the async model.
Quick Revision Cheat Sheet
all: Resolves with an ordered array; rejects on first failure
allSettled: Always resolves with {status, value/reason} objects
race: Settles with the first promise to settle, success or failure
Order: Write to results[index], never push, to keep input order
Normalise: Wrap inputs in Promise.resolve to allow plain values
Edge case: Resolve immediately for an empty iterable