Promise.all vs allSettled vs race vs any
By Flavio Copes
Learn when to use Promise.all, allSettled, race and any in JavaScript, how each handles rejections, and the common mistakes that bite in production.
JavaScript gives us 4 ways to combine multiple promises: Promise.all(), Promise.allSettled(), Promise.race() and Promise.any().
They look similar. They behave very differently when something fails.
I wrote about promises and waiting for multiple promises before. This post is about picking the right combinator, because picking the wrong one is a classic source of subtle bugs.
Here’s the short version:
Promise.all()— wait for everything, fail fast on the first rejectionPromise.allSettled()— wait for everything, never failPromise.race()— take the first one that settles, win or losePromise.any()— take the first one that fulfills, ignore failures
Let’s go through each one.
Promise.all: everything must succeed
Use Promise.all() when you need every result, and one failure should abort the whole operation.
The classic case is loading data where each piece is required:
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
])
If both fulfill, you get an array of results in the same order you passed the promises in. Not the order they finished in.
If any promise rejects, Promise.all() rejects immediately with that error. This is called failing fast.
There’s a detail people miss: the other promises keep running. Promise.all() doesn’t cancel anything, because promises can’t be cancelled. It just stops waiting. If fetchUsers() fails after 100ms, your await throws at 100ms, but fetchPosts() still completes in the background. Its result is thrown away.
I covered the basics of this pattern in how to wait for all promises to resolve, so here I’ll just add my advice: use Promise.all() as your default. Most of the time, if one required thing fails, you can’t do useful work anyway.
Promise.allSettled: collect every outcome
Sometimes partial success is fine. You’re sending 10 notification emails, and you don’t want one bad address to hide the 9 that worked.
Promise.allSettled() waits for every promise to settle, and never rejects:
const results = await Promise.allSettled([
sendEmail('[email protected]'),
sendEmail('bad-address'),
])
Each result is an object with a status field:
[
{ status: 'fulfilled', value: undefined },
{ status: 'rejected', reason: Error }
]
You have to inspect each one:
for (const result of results) {
if (result.status === 'fulfilled') {
console.log('sent:', result.value)
} else {
console.log('failed:', result.reason)
}
}
Notice that fulfilled results have a value and rejected ones have a reason. Mixing those up is a common typo that TypeScript catches and plain JavaScript silently doesn’t.
Promise.race: first to settle wins
Promise.race() settles as soon as the first promise settles. Fulfilled or rejected, doesn’t matter.
The classic use case is a timeout:
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('timeout')), 5000)
})
const data = await Promise.race([fetchUsers(), timeout])
If fetchUsers() finishes within 5 seconds, you get the data. Otherwise the timeout rejects first and the await throws.
Be careful with the semantics here. If the fastest promise rejects, race() rejects, even if a slower promise would have succeeded a moment later. Race means race: the first one across the line decides the outcome, win or lose.
If that’s not what you want, you want Promise.any().
Promise.any: first success wins
Promise.any() fulfills with the first promise that fulfills. Rejections are ignored, unless everything rejects.
Think of fetching the same file from multiple mirrors:
const file = await Promise.any([
fetch('https://cdn-eu.example.com/app.js'),
fetch('https://cdn-us.example.com/app.js'),
])
If the EU mirror is down, no problem. As long as one mirror responds, you get its result.
If every promise rejects, Promise.any() rejects with an AggregateError. This is a special error that wraps all the individual failures:
try {
await Promise.any([failA(), failB()])
} catch (err) {
console.log(err instanceof AggregateError) //true
console.log(err.errors) //[Error from failA, Error from failB]
}
The individual errors are in the errors array. Log them, or you’ll be debugging blind.
Common mistakes
Unhandled rejections with race()
This one bites in production. Say you race a fetch against a timeout, and the fetch wins:
const data = await Promise.race([fetchUsers(), timeoutIn(5000)])
The timeout promise is still out there. When it rejects 5 seconds later, nothing is listening. In Node.js that’s an unhandled rejection, and depending on your setup it can crash the process.
The fix is to make sure every promise you create has a rejection handler, or to make the timeout resolve instead of reject and check the result. AbortController with AbortSignal.timeout() is often the cleaner solution for fetch specifically.
Awaiting in a loop instead of combining
If your operations don’t depend on each other, don’t await them one by one:
//slow: sequential
const users = await fetchUsers()
const posts = await fetchPosts()
//fast: parallel
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()])
The async/await syntax makes sequential code so easy to write that people write it by accident.
Forgetting that allSettled never throws
Wrapping Promise.allSettled() in a try/catch does nothing. It never rejects. If you’re not checking each result’s status, failures pass through your code silently.
Which one should you use?
Ask two questions.
Do you need all results? If yes: all() when a failure should abort, allSettled() when partial success is fine.
Is one result enough? If yes: race() when you want the fastest settlement (timeouts), any() when you want the first success (fallbacks, mirrors).
I built a small Promise combinator chooser and visualizer that walks you through these questions and shows on a timeline how each combinator settles with the same set of promises. Try flipping one promise to “reject” and watch how differently the four behave.
Related posts about js: