Promise.race()

ES6+

Returns a promise that fulfills or rejects as soon as one of the promises fulfills or rejects.

Syntax

Promise.race(iterable)

Parameters

iterable Iterable

An iterable of promises

Return Value

Promise

A Promise that settles with the first settled promise

Examples

JavaScript
const slow = new Promise(r => setTimeout(() => r('느림'), 500));
const fast = new Promise(r => setTimeout(() => r('빠름'), 100));

Promise.race([slow, fast])
  .then(value => console.log(value));
Output:
// '빠름'

📌 When to Use

Use Promise.race() when the first settlement — success or failure — should decide the outcome. Its flagship use is enforcing timeouts on operations that lack native deadline support: race the real work against a timer that rejects after N milliseconds, and slow requests turn into prompt, handleable errors instead of spinners that never resolve. It also fits watchdog patterns — race a task against a cancellation promise you control, or against a 'user navigated away' signal — and latency heuristics, such as racing the data fetch against a 200 ms timer to decide whether a loading indicator is worth showing at all. Remember the semantics precisely: the first settled promise wins, and if that first settlement is a rejection, race() rejects even though a sibling might have succeeded a millisecond later. When you want the first success, with rejections ignored unless every input fails, that is Promise.any() instead. And because the losing promises are not cancelled — they keep running and holding resources — production-grade timeout code should pair race() with AbortController so the abandoned operation is actually torn down rather than merely ignored. For fetch timeouts specifically, modern runtimes offer AbortSignal.timeout(), which expresses the whole pattern in one line.

⚠️ Common Mistakes

Forgetting that the race can be lost to an error: if the first promise to settle rejects, race() rejects immediately, even if another input was about to fulfill a moment later. For 'first success wins' semantics, use Promise.any() instead.

Passing an empty array. Promise.race([]) is pending forever — nothing can ever settle it — so any await on it hangs the surrounding function silently, with no error and no timeout. Guard input lists that are built dynamically.

The classic timeout memory leak: the losing side keeps its resources alive. If the fetch wins, the setTimeout still fires later, holding its closure until then; if the timer wins, the request keeps streaming and its pending promise retains handlers and buffers. In servers running thousands of races, abandoned timers and sockets accumulate into real memory pressure. Clear the timer in a finally() and abort the request.

Assuming the losers stop. race() only stops listening; it cancels nothing. An expensive query that lost the race still completes on the backend, and code with side effects — writes, cache fills, counters — still executes them, sometimes after you have already handled the timeout path. Design raced work to be abortable or idempotent.

Racing promises that share a resource, like two readers of the same response body or stream. The loser has often already consumed or locked the resource, so the winning path fails later with confusing errors such as 'body stream already read'.

✅ Best Practices

Implement timeouts by racing against a rejecting timer — Promise.race([fetchData(), timeout(5000)]) — but keep the timer id and clearTimeout() it in a finally(), so the timeout machinery never outlives the request it was guarding.

Reach for Promise.any() when rejections should be ignored until every candidate has failed; reserve race() for cases where the first settlement of either kind is genuinely decisive, like deadlines and cancellation.

Pair every race with AbortController (or the operation's own cancellation API) and abort the losers as soon as a winner settles. On modern runtimes, AbortSignal.timeout(ms) expresses the entire fetch-timeout pattern natively, without a manual race.

Reject the timeout branch with a distinctive Error subclass such as TimeoutError, so callers can distinguish 'too slow' from 'failed' and choose different retry and messaging strategies for each.

Keep raced operations free of side effects until the outcome is known where possible: fetch data inside the race, but apply state changes only after the winner is decided, so a late-finishing loser cannot corrupt application state.

⚡ Performance Notes

race() subscribes one reaction to each input and settles with the first result — after that it does no further work, but everything else keeps running. The performance story is therefore about the losers: a pending promise retains its reaction records, its closure variables, and whatever the underlying operation holds (sockets, buffers, timers) until it settles naturally. The combinator itself does not leak — engines collect forever-pending promises once they become unreachable — but the timer in the typical timeout pattern is a live reference held by the event loop until it fires, and an un-aborted fetch is a real open connection. Under load, thousands of five-second timers surviving already-completed requests amount to measurable memory and CPU churn. Settlement is delivered on the microtask queue like any then(), and the single extra promise race() allocates is trivial. Clear timers and abort losers, and race() adds effectively zero overhead of its own.

🌍 Real World Example

Fetch with Timeout

A timeout wrapper for fetch(): the request races a timer that rejects with a descriptive error once the deadline passes, so callers get either the response or a clear 'timed out' failure they can present differently from a network error. This minimal version demonstrates the pattern — production code should additionally clearTimeout the timer when the fetch wins and pass an AbortController signal so a timed-out request is truly cancelled instead of continuing to download in the background; AbortSignal.timeout() packages both refinements into one call on modern runtimes.

function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const fetchPromise = fetch(url, options);

  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Request timed out after ${timeoutMs}ms`));
    }, timeoutMs);
  });

  return Promise.race([fetchPromise, timeoutPromise]);
}

// Usage
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
  .then(response => response.json())
  .then(data => console.log('Success:', data))
  .catch(error => {
    if (error.message.includes('timed out')) {
      console.log('Request took too long');
    } else {
      console.log('Network error:', error.message);
    }
  });

Related Methods