Promise.race() settles as soon as the first input promise settles — whether that's a success or a failure. This makes it the standard building block for implementing request timeouts.
1Promise.race() Deep Dive | JavaScript Tutorial - In-Depth Guide Part 1
Promise.race() settles the instant the first input promise settles, adopting that same outcome — fulfilling if the first one fulfills, or rejecting if the first one rejects.
const first = await Promise.race([
delay(100).then(() => 'fast'),
delay(50).then(() => Promise.reject('quick failure')),
]);
// rejects with 'quick failure', since it settled firstFirst to Settle, Win or Lose
2Promise.race() Deep Dive | JavaScript Tutorial - In-Depth Guide Part 2
The most common real-world use of Promise.race() is implementing a timeout: race a real operation against a promise that rejects after a fixed delay.
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}The Timeout Pattern
3Promise.race() Deep Dive | JavaScript Tutorial - In-Depth Guide Part 3
The losing promise in a race() is not cancelled — it keeps running and its eventual result (or side effects) are simply ignored.
// If the timeout wins, the actual fetch() keeps running:
await withTimeout(fetch('/api/slow'), 3000); // throws 'Timeout'
// but the underlying fetch() request is still in flightLosers Aren't Cancelled
4Promise.race() Deep Dive | JavaScript Tutorial - In-Depth Guide Part 4
Combining Promise.race() with AbortController lets you both enforce a timeout AND actually stop the underlying operation when it loses the race.
function fetchWithTimeout(url, ms) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ms);
return fetch(url, { signal: controller.signal }).finally(() => clearTimeout(timeout));
}Racing + Actually Cancelling
5Promise.race() Deep Dive | JavaScript Tutorial - In-Depth Guide Part 5
Promise.race() on an empty array never settles — there is no 'first' promise to determine an outcome, so the returned promise stays pending forever.
Promise.race([]); // pending forever, never settlesEmpty Array Never Settles
6Step-by-Step Breakdown
Promise.race() settles the instant the first input promise settles, adopting that same outcome — fulfilling if the first one fulfills, or rejecting if the first one rejects.
Checkpoint: If the fastest promise passed to Promise.race() rejects, does race() reject too?
- →Yes, race() adopts whatever outcome settles first
- →No, race() only rejects if every promise rejects
The most common real-world use of Promise.race() is implementing a timeout: race a real operation against a promise that rejects after a fixed delay.
The losing promise in a race() is not cancelled — it keeps running and its eventual result (or side effects) are simply ignored.
Checkpoint: If a real fetch() request loses a Promise.race() against a timeout, does the actual network request stop?
- →Yes, race() automatically cancels the loser
- →No, it keeps running unless separately aborted
Combining Promise.race() with AbortController lets you both enforce a timeout AND actually stop the underlying operation when it loses the race.
Promise.race() on an empty array never settles — there is no 'first' promise to determine an outcome, so the returned promise stays pending forever.
Next, we'll explore 'Promise.any() Deep Dive'.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Show a Clear Timeout Message, Not a Silent Failure, When race()'s Timeout Wins
When a race()-based timeout triggers, ensure the resulting error state is announced accessibly (e.g. via an ARIA live region: 'Request timed out, please try again') rather than leaving the UI in an ambiguous loading or blank state.
SEO Implications
- 1
Timeouts Prevent Hung Requests from Blocking Server-Side Rendering
Using a race()-based timeout around external data fetches during server-side rendering prevents a single slow upstream API from stalling page generation indefinitely, protecting Time to First Byte.
Best Practices
Always Pair a Timeout race() with an AbortController for Real Cancellation
A timeout that only stops your code from waiting, without stopping the underlying request, wastes network/server resources on work whose result will be discarded.
Clear the Timeout Once the Real Operation Wins
Failing to clearTimeout() after the real promise resolves first leaves a dangling timer that will fire uselessly later — always clean it up in a .finally() block.
Frequent Bugs
Implementing a timeout with race() but forgetting to also cancel the underlying operation, so a 'timed out' request keeps consuming bandwidth and server resources in the background.
Wire the timeout to call an AbortController's abort() method, and pass its signal into the real operation (like fetch), so losing the race also actually stops the work.
Leaving a timeout's setTimeout uncleared after the real promise wins the race, causing a stray timer to fire later for no reason.
Use `.finally(() => clearTimeout(timeoutId))` on the raced promise to guarantee cleanup regardless of which side wins.
Real-World Examples
Enforcing a Maximum Wait Time on a Slow Third-Party API
An app integrated with a third-party API that occasionally hung indefinitely, and needed to guarantee a response (or a clear timeout error) within 5 seconds.
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
}