Last week, a flatmate of mine — a seasoned Java/Spring backend developer — knocked on my door with a quick frontend question. He'd been handed a small task on the frontend side of his project, nothing major, and had reached out for a sanity check before pushing his code.
He had used Promise.race to implement a fallback — if the primary API failed, he wanted to use the backup. The logic made sense in his head. But the implementation was wrong, and the bug was subtle enough that it had slipped through his initial testing.
When I asked him "do you know the difference between race and any?", he paused. And honestly? The pause made me think. Because two years ago, I would have paused too.
That conversation sent me down a rabbit hole — rebuilding polyfills for all four Promise methods — all, any, race, and allSettled — from scratch, just to make sure I truly understood the internals and not just the surface-level API.
Now, there are already separate blog posts floating around on each of these (including some I wrote a couple of years ago 😄). But the real gap isn't understanding each method individually — it's knowing how they relate to each other and which one to reach for in a given situation.
So this is that blog where I wanted to share my learning. The consolidated one. The "one ring to rule them all" of Promise methods. 💍
The Quick Cheat Sheet (For the Impatient 😄)
| Method | Resolves when | Rejects when | Returns |
|---|---|---|---|
Promise.all |
All resolve | Any one rejects | Array of values (ordered) |
Promise.any |
Any one resolves | All reject | First resolved value |
Promise.race |
Any one settles | Any one rejects first | First settled value |
Promise.allSettled |
All settle (always resolves) | Never | Array of {status, value/reason}
|
The Decision Framework
Before the deep dive, here's the mental model I use when picking between these four:
Do you need ALL to succeed? →
Promise.allDo you need just ONE to succeed, ignoring the rest? →
Promise.anyDo you want whoever finishes first — win or lose? →
Promise.raceDo you want the full picture regardless of success or failure? →
Promise.allSettled
If that's all you needed, you're welcome. 😄 For everyone else, let's go deeper.
Promise.all
What it does
Takes an array of promises and returns a single promise that resolves with an array of all resolved values, in the same order as the input. If any one promise rejects, the whole thing rejects immediately — it doesn't wait for the rest.
const promise1 = Promise.resolve('Hello');
const promise2 = Promise.resolve(42);
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'World'));
Promise.all([promise1, promise2, promise3])
.then((values) => console.log(values))
.catch((error) => console.error(error));
// Output: ["Hello", 42, "World"]
When to use it
- Fetching data from multiple APIs where all results are required to render (think dashboards)
- Uploading multiple files and waiting for all to complete before showing a success message
- Batch database operations where all must succeed together
Polyfill
function all(promises) {
function executorFunction(resolve, reject) {
const result = [];
let pendingCount = promises.length;
if (pendingCount === 0) {
resolve(result);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then((value) => {
result[index] = value; // 👈 index assignment, not push
if (--pendingCount === 0) {
resolve(result);
}
}, reject); // first rejection short-circuits everything
});
}
return new Promise(executorFunction);
}
⚠️ The gotcha hiding in plain sight: Notice I use
result[index] = valueand NOTresult.push(value). If I usedpush, the order would depend on which promise resolved first — not the original input order.Promise.allguarantees output order matches input order, and this one line is why. ThependingCountcounter is how I know when every promise has settled without needingawait.
Promise.any
What it does
Returns a promise that resolves with the first fulfilled value. Rejections are silently ignored — unless all promises reject, in which case it throws an AggregateError wrapping all the rejection reasons.
const promise1 = Promise.reject(new Error('First failed'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success'));
const promise3 = Promise.reject(new Error('Second failed'));
Promise.any([promise1, promise2, promise3])
.then((value) => console.log(value))
.catch((error) => console.error(error));
// Output: "Success"
When to use it
- Trying multiple CDN sources and using whichever responds first
- Primary + backup service pattern — use the backup only if primary fails
- Optimistic UI — show cached data or fresh API data, whoever arrives first wins
Polyfill
function any(promises) {
function executorFunction(resolve, reject) {
const errors = [];
let pendingCount = promises.length;
if (pendingCount === 0) {
reject(new AggregateError(errors, 'No promises were passed'));
return;
}
promises.forEach((promise, idx) => {
Promise.resolve(promise)
.then((val) => resolve(val)) // first resolve wins
.catch((err) => {
errors[idx] = err; // preserve order of errors
if (--pendingCount === 0) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
}
return new Promise(executorFunction);
}
⚠️ Two gotchas here:
Gotcha 1 — AggregateError is not a regular Error: When all promises reject, the catch block receives an
AggregateError. Don't try to readerror.messageexpecting individual reasons — accesserror.errors(the array) to get each rejection reason.Gotcha 2 — Empty array behavior:
Promise.any([])immediately rejects withAggregateError. This is the opposite ofPromise.all([]), which immediately resolves with an empty array. Catch this during interviews — it's a classic trap. 😄
Notice the symmetry with the Promise.all polyfill — but inverted. In all, a single rejection short-circuits. In any, a single resolution short-circuits. We still use indexed assignment for errors (not push) so the error order matches the input order, which matters when you're debugging why everything blew up.
Promise.race
What it does
Returns a promise that settles as soon as the first promise settles — resolve or reject. Whoever finishes first, wins. The others are abandoned.
const promise1 = new Promise((resolve) => setTimeout(resolve, 200, 'Slow'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Fast'));
Promise.race([promise1, promise2])
.then((value) => console.log(value))
.catch((error) => console.error(error));
// Output: "Fast"
When to use it
The classic use case — implementing a timeout for any async operation:
const fetchData = () => fetch('https://api.example.com/data');
const timeout = (ms) =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timed out')), ms),
);
Promise.race([fetchData(), timeout(5000)])
.then((response) => response.json())
.catch((error) => console.error(error));
// Rejects with "Request timed out" if API doesn't respond in 5 seconds
Polyfill
function race(promises) {
function executorFunction(resolve, reject) {
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject);
}
}
return new Promise(executorFunction);
}
This is the simplest polyfill of the four — and the elegance is worth appreciating. Once a Promise settles, subsequent calls to resolve or reject are silently ignored by the JS engine. So we just attach handlers to every promise and let the first one win naturally. No counter, no result array, no complexity.
⚠️ The race vs any confusion: This is the most common mixup I see.
race= first to finish (resolve OR reject). A rejection wins the race too.any= first to succeed. Rejections are ignored until all fail.If the first promise to settle in
Promise.raceis a rejection, the whole thing rejects — it doesn't wait for a successful one. If that's not what you want, you probably wantPromise.any.⚠️ Empty array is a silent trap:
Promise.race([])returns a promise that is permanently pending — it never settles. UnlikePromise.all([])which resolves immediately, orPromise.any([])which rejects immediately,racejust waits forever. Always guard against this in production code:if (promises.length === 0) return Promise.reject(new Error('No promises provided'));
Promise.allSettled
What it does
Waits for all promises to settle and returns an array of objects describing each outcome. It never rejects — you always get the full picture, wins and losses both.
const promise1 = Promise.resolve('Hello');
const promise2 = Promise.reject(new Error('Something went wrong'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'World'));
Promise.allSettled([promise1, promise2, promise3]).then((results) =>
console.log(results),
);
// Output:
// [
// { status: "fulfilled", value: "Hello" },
// { status: "rejected", reason: Error: Something went wrong },
// { status: "fulfilled", value: "World" }
// ]
When to use it
- Fetching from multiple APIs where partial success is acceptable — show what you have, log what failed
- Batch processing where some failures are expected and shouldn't stop the rest
- Running multiple independent operations and reporting the full outcome
Polyfill
function allSettled(promises) {
function executorFunction(resolve) {
// ☝️ no reject parameter — we always resolve
const result = [];
let pendingCount = promises.length;
if (pendingCount === 0) {
resolve(result);
return;
}
promises.forEach((promise, idx) => {
Promise.resolve(promise)
.then((value) => (result[idx] = { status: 'fulfilled', value }))
.catch((reason) => (result[idx] = { status: 'rejected', reason }))
.finally(() => {
if (--pendingCount === 0) {
resolve(result);
}
});
});
}
return new Promise(executorFunction);
}
⚠️ Implementation insight: The executor function deliberately omits the
rejectparameter — this promise should never reject, so why even have it there? The.finally()is the cleanest way to decrement the counter regardless of whether.thenor.catchran. Both write toresult[idx]before.finallyfires, so by the time we callresolve, every slot is already filled.
The Side-by-Side That Makes It Click
Let me pass the same promises to all four and you'll immediately see the difference:
const fast = Promise.resolve('fast');
const slow = new Promise((res) => setTimeout(res, 200, 'slow'));
const fail = Promise.reject(new Error('failed'));
// Promise.all — one failure, everything fails
Promise.all([fast, slow, fail]);
// ❌ Rejects with Error: "failed"
// Promise.any — one success is enough
Promise.any([fast, slow, fail]);
// ✅ Resolves with "fast"
// Promise.race — fail settles first (it's already rejected)
Promise.race([fail, fast, slow]);
// ❌ Rejects with Error: "failed"
// Promise.allSettled — never rejects, full picture
Promise.allSettled([fast, slow, fail]);
// ✅ Resolves with:
// [
// { status: 'fulfilled', value: 'fast' },
// { status: 'fulfilled', value: 'slow' },
// { status: 'rejected', reason: Error: 'failed' }
// ]
Common Mistakes (That I've Also Made 😅)
1. Reaching for Promise.all when partial failure is fine
If your dashboard can render with 3 out of 4 APIs responding, use allSettled and handle failures individually. Promise.all will discard all results the moment one fails. Most real-world scenarios are more forgiving than Promise.all assumes.
2. Confusing race and any
race = first to finish. any = first to succeed. If you want a timeout that only triggers when your request actually fails (not just when it's slower than a competing promise), you want any not race.
3. Forgetting AggregateError in Promise.any
Your catch block for Promise.any will receive an AggregateError when all promises reject. Accessing error.message won't give you the individual reasons. You need error.errors — the array of all rejection reasons.
4. Using push instead of index assignment in custom implementations
If you ever write a custom aggregator and use result.push(value) instead of result[index] = value, you'll silently break the order guarantee. The output order will depend on resolution speed, not input order. And this will only show up in production under load. Fun times. 😄
Quick Reference
| Method | Use when... |
|---|---|
Promise.all |
You need ALL results and one failure should abort everything |
Promise.any |
You need ONE success and don't care which one |
Promise.race |
You want the first to settle — success or failure |
Promise.allSettled |
You want all outcomes and will handle each individually |
Building these polyfills from scratch was genuinely one of the better learning exercises I've done recently. Reading docs tells you what these methods do. Writing the internals tells you why they behave the way they do — and that's where the real understanding lives.
Until next time, Pranipat 🙏!
Top comments (0)