Trying to re-create a feature can be a great way to understand it. Let's do that with Promise.race()
!
Requirements
Promise.race has perhaps the simplest requirements:
…takes an iterable of promises as input and returns a single Promise. This returned promise settles with the eventual state of the first promise that settles.
The most common use is with an Array, so we'll start with that assumption, and we can build up the features.
- Takes an array of promises
- Returns one promise
- First settled state is used
RunJS
I use RunJS for coding exercises like this. I'm not paid or incentivized to promote it; it's just a good playground for code. All my code samples are written or tested in RunJS.
Basic Implementation
This is actually very easy to implement because Promises have one very important characteristic: they can only be settled once. So whatever gets there first will win. We just have to iterate over the array.
const race = (promises) => new Promise((resolve, reject) => {
promises.forEach(promise => promise.then(resolve, reject));
});
At its most basic, that is all Promise.race()
is doing. A new promise is created. Resolve and reject are passed to each promise in the iterator. First one wins.
Done!
const a = Promise.resolve(1);
const b = Promise.resolve(2);
Promise.race([a, b]).then(console.log);
// 1
race([a, b]).then(console.log);
// 1
Well, sort-of. There are a number of cases we don't currently cover.
Non-Promise Entries
Promise.race([1, 2]).then(console.log);
// 1
race([1, 2]).then(console.log);
// Error: 'promise.then is not a function'
While the high-level description on MDN doesn't say so, Promise.race()
accepts non-promise values. These are converted to resolved promises. This can be very helpful in testing scenarios, as you can build simple synchronous mocks of functions and not worry that the output of functions passed to Promise.race()
or Promise.all()
are themselves promises.
So how do we make sure everything becomes a promise? Promises have already solved this issue for us, with Promise.resolve()
.
Resolve Everything?
Promise.resolve()
checks for a promise, and passes it along unchanged. So any existing promise – including rejected ones – will make it through. Non-promises will become resolved promises, which is what we need.
Non-Promise Support
const race = (promises) => new Promise((resolve, reject) => {
promises.forEach(promise => Promise.resolve(promise)
.then(resolve, reject));
});
Now we can safely pass in non-promise values.
Promise.race(['red', 'blue']).then(console.log);
// 'red'
race(['red', 'blue']).then(console.log);
// 'red'
Iterables
Right now our implementation depends on the .forEach
method, which works for arrays, Sets and Maps, but not for everything. It's a less common scenario, but this is valid:
Promise.race('ABC').then(console.log);
// 'A'
A string is iterable, but it doesn't have a forEach
method, so it doesn't work with our implementation.
race('ABC').then(console.log);
// Error: 'promises.forEach is not a function'
There are a couple ways we could resolve this... using for...of
to iterate:
const race = (promises) => new Promise((resolve, reject) => {
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject);
}
});
Or, if we want to stick with what's familiar, Array.from()
leaves us with an array so we can still use .forEach()
.
const race = (promises) => new Promise((resolve, reject) => {
Array.from(promises).forEach(promise =>
Promise.resolve(promise)
.then(resolve, reject));
});
Now we support any iterable.
Promise.race('ABC').then(console.log);
// 'A'
race('ABC').then(console.log);
// 'A'
Conclusion
MDN provides a lot of useful information an examples on, well, everything, so be sure to check out the docs if you haven't already.
I hope you've enjoyed this quick trip through Promise.race()
. Breaking down a piece of functionality to identify the requirements and the features is a useful skill to hone, and re-creating easy-to-verify functionality like this can be a great way to build that skill.
I'd love to hear about the tests and examples you've created/re-created over time!
Top comments (1)
Nice guide, I used Promise.race to add timeouts to fetch calls.