0%

Handwriting Promise.race (From Theory to Practice)

In frontend interviews and daily development, Promise.race is a very common yet often overlooked API. Its rule is simple:

Multiple Promises “race” against each other—whichever settles first (fulfilled or rejected) determines the final result.

In this article, we will:

  • Clearly explain how Promise.race behaves and when to use it
  • Implement a custom myPromiseRace from scratch
  • Cover edge cases, optimizations, and common interview questions

1. What Is Promise.race?

Promise.race(iterable) accepts an iterable (usually an array) and returns a new Promise:

  • If any Promise fulfills first, the returned Promise fulfills
  • If any Promise rejects first, the returned Promise rejects
  • It does not wait for all Promises to finish
  • It does not cancel the remaining Promises—they continue running

2. Common Use Cases

2.1 Timeout Control (Most Common)

For example, treat a request as failed if it takes longer than 3 seconds:

  • fetch(url) vs timeoutPromise(3000)
  • Whichever finishes first determines the result

2.2 Multiple Data Sources (Fallback Strategy)

Fetch the same resource from multiple CDNs (CDN1/CDN2).
Use whichever responds first.

2.3 First-Render Racing

Load primary data, fallback data, and cached data simultaneously.
Render the page as soon as the fastest result arrives.

3. Implementing myPromiseRace

This implementation closely matches native behavior:

  • Validates that the input is iterable
  • Iterates over all inputs
  • Uses Promise.resolve to normalize non-Promise values
  • Resolves or rejects as soon as one Promise settles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function myPromiseRace(promises) {
return new Promise((resolve, reject) => {
// Validate iterable input
if (!promises || typeof promises[Symbol.iterator] !== 'function') {
return reject(new TypeError('Argument is not iterable'));
}

// Iterate over each Promise or value
for (const promise of promises) {
// Wrap with Promise.resolve to handle thenables and values
Promise.resolve(promise)
.then(resolve) // First fulfilled wins
.catch(reject); // First rejected wins
}
});
}

4. Key Insight: Why Use Promise.resolve?

The input to Promise.race does not have to be Promises. It may include:

  • Plain values: Promise.race([1, 2, 3])
  • Thenables: { then(resolve) { resolve(123) } }

Using Promise.resolve(x) ensures:

  • Plain values become immediately fulfilled Promises
  • Thenables are properly assimilated into the Promise chain

This behavior is consistent with the native Promise.race.

5. Handling Empty Iterables

Native Promise.race([]) returns a Promise that remains pending forever.

Your current implementation behaves the same way, since the loop never executes.
This matches the ECMAScript specification.

You could add explicit handling, but it’s not required.

6. Additional Test Cases

6.1 Rejection Wins First

1
2
3
4
5
6
7
const p1 = new Promise(res => setTimeout(() => res('p1'), 1000));
const p2 = new Promise(res => setTimeout(() => res('p2'), 500));
const p3 = new Promise((res, rej) => setTimeout(() => rej('p3 error'), 300));

myPromiseRace([p1, p2, p3])
.then(console.log)
.catch(console.error); // Output: p3 error

6.2 Plain Values Participate in the Race

1
2
3
4
5
6
7
8
9
myPromiseRace([
Promise.resolve('A'),
123,
new Promise(res => setTimeout(() => res('B'), 10))
])
.then(console.log)
.catch(console.error);

// Output: 123 (plain value wins immediately)

6.3 Empty Array Remains Pending

1
2
3
const p = myPromiseRace([]);
setTimeout(() => console.log('still pending...'), 1000);
// After 1 second: "still pending..."

7. Practical Example: Request Timeout with race

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
}

async function fetchWithTimeout(url, ms = 3000) {
return myPromiseRace([fetch(url), timeout(ms)]);
}

// Usage
fetchWithTimeout('https://example.com/api', 2000)
.then(res => res.json())
.then(data => console.log('data:', data))
.catch(err => console.error('error:', err.message));

8. Common Interview Questions (Bonus Points)

  • Q: Can Promise.race cancel the other Promises?
    No. Promises are not cancellable by default. Cancellation requires tools like AbortController or custom logic.

  • Q: What happens if you pass a plain value into race?
    It is wrapped by Promise.resolve and usually wins immediately.

  • Q: What’s the difference between race, any, and allSettled?

    • race: settles as soon as the first Promise settles
    • any: fulfills as soon as the first Promise fulfills (rejects only if all reject)
    • allSettled: waits for all Promises and returns their final states

9. Summary

The core idea behind implementing Promise.race can be summarized in one sentence:

Iterate over the iterable, wrap each item with Promise.resolve, attach the same resolve and reject, and let the first settled Promise decide the outcome.

-------------本文结束感谢您的阅读-------------