DEV Community

Samuel Rouse
Samuel Rouse

Posted on

Quick! Let's Build Promise.race()

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));
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' 
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

Now we can safely pass in non-promise values.

Promise.race(['red', 'blue']).then(console.log);
// 'red'

race(['red', 'blue']).then(console.log);
// 'red'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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' 
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

Now we support any iterable.

Promise.race('ABC').then(console.log);
// 'A'

race('ABC').then(console.log);
// 'A'
Enter fullscreen mode Exit fullscreen mode

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 (0)