My colleague, Erik Timmers, and I often have discussions about programming and related technologies. This blog post is the result of one of those discussions. We discovered that the function Promise.race didn’t exactly do what we expected. So we tested it, figured out how it worked, found out what we thought was wrong, and finally created a version of the Promise.race function that does what we expected. After that we went a little bit further…and added some functionality to the function. Please note that this code shouldn’t be used in production, or at the very least, it should be tested a bit more. We did it “because we could”, but also because we wanted to understand the functionality. If you would like to view, extend, learn from the actual code, the source code is also available on GitHub.

Before we go any further, if you’re not sure what JavaScript Promises are, read this post. It’s a really great introduction for Promises. Furthermore, the code examples in this post uses Arrow functions, if you are unfamiliar with them, check out this link for an explanation.

So what’s wrong with Promise.race?

So you’re working on a JavaScript application, and your IntelliJ (because why would you use any other IDE?) autocomplete pops up with the function ‘race’ when you write ‘Promise.’. So without looking at the documentation, what would you expect Promise.race to do?
My first though was, that you can probably pass a few promises to this function and it returns the value of the first promise that resolved. Makes sense right?

let car1 = new Promise(resolve => setTimeout(resolve, 2000, 'Car 1.'));
let car2 = new Promise(resolve => setTimeout(resolve, 1000, 'Car 2.'));
let car3 = new Promise(resolve => setTimeout(resolve, 4000, 'Car 3.'));
let car4 = new Promise(resolve => setTimeout(resolve, 9000, 'Car 4.'));
let car5 = new Promise(resolve => setTimeout(resolve, 8000, 'Car 5.'));

Promise.race([car1, car2, car3, car4, car5])
  .then(val => console.log('Race complete! The winner is:', val))
  .catch(err => console.log('Race ended because: ', err));

As you would expect, the output is:
Race complete! The winner is: Car 2.

Now to make things a bit more interesting, what would happen if ‘Car 2’ crashed? You would expect ‘Car 1’ to win:

let car1 = new Promise(resolve => setTimeout(resolve, 2000, 'Car 1.'));
let car2 = new Promise((resolve, reject) => setTimeout(reject, 1000, 'CAR 2 CRASHED!'));
let car3 = new Promise(resolve => setTimeout(resolve, 4000, 'Car 3.'));
let car4 = new Promise(resolve => setTimeout(resolve, 9000, 'Car 4.'));
let car5 = new Promise(resolve => setTimeout(resolve, 8000, 'Car 5.'));

Promise.race([car1, car2, car3, car4, car5])
  .then(val => console.log('Race complete! The winner is:', val))
  .catch(err => console.log('Race ended because: ', err));

Let’s look at the output:
Race ended because: CAR 2 CRASHED!

Wait, what? All other cars are ignored, and the the whole ‘race’ is ended.
That doesn’t make sense… Let’s look at the documentation to see what’s going on:

The Promise.race(iterable) method returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.

Wow… it’s intended to do this.
In the case of Promise.all it would make sense – you want them all to resolve, so if one rejects, why bother to go on? But in my opinion this is just really weird functionality for a function named ‘race’. Think of it like this: let’s say you want to get some data from one of many databases. You don’t care which one, you just want one result. That would mean that if one database is down, you wouldn’t get a result at all.

It gets worse

Let’s look at Promise.all for a second.
What happens if we pass no promises to Promise.all?

Promise.all([]).then((val) => console.log('Resolved!', val))
  .catch((err) => console.log('Rejected!', err));

The output:
Resolved!

That makes sense. As we want to wait untill everything has resolved and there is nothing to wait for, it resolves the Promise.all.

So what happens if we pass no promises to Promise.race? In my opinion, the most logical outcome would be that the race returns ‘Rejected!’, because why bother to start a race if there is nothing to race?

Promise.race([]).then((val) => console.log('Resolved!', val))
  .catch((err) => console.log('Rejected!', err));

The output? There is none…

If we look at the Promise object that is returned by race, we can see it has the status ‘pending’. That means this never resolves, and we would be waiting forever.

Fixing the flaws

The first step in fixing this function is to reject if there aren’t enough promises. Because why bother starting a race, if there is nothing to race?

Promise.properRace = function(promises) {
    if (promises.length < 1) {
      return Promise.reject('Can\'t start a race without promises!');
    }
  return Promise.race(promises);
}
// Testing it:
Promise.properRace([]).then((val) => console.log('Resolved!', val))
  .catch((err) => console.log('Rejected!', err));

The output:
Rejected! Can’t start a race without promises!

That’s more like it. Next step, make sure a race doesn’t end when one car ‘crashes’. To tackle the problem, we need to find a way to figure out which Promise rejected, so we can ignore it.

Promise.properRace = function(promises) {
  if (promises.length < 1) {
    return Promise.reject('Can\'t start a race without promises!');
  }

  // There is no way to know which promise is rejected.
  // So we map it to a new promise to return the index when it fails
  let indexPromises = promises.map((p, index) => p.catch(() => {throw index;}));

  return Promise.race(indexPromises).catch(index => {
    // The promise has rejected, remove it from the list of promises and just continue the race.
    let p = promises.splice(index, 1)[0];
    p.catch(e => console.log('A car has crashed, don\'t interrupt the race:', e));
    return Promise.properRace(promises);
  });
};

let car1 = new Promise(resolve => setTimeout(resolve, 2000, 'Car 1.'));
let car2 = new Promise((resolve, reject) => setTimeout(reject, 1000, 'CAR 2 CRASHED!'));
let car3 = new Promise(resolve => setTimeout(resolve, 4000, 'Car 3.'));
let car4 = new Promise(resolve => setTimeout(resolve, 9000, 'Car 4.'));
let car5 = new Promise(resolve => setTimeout(resolve, 8000, 'Car 5.'));

Promise.properRace([car1, car2, car3, car4, car5])
    .then(val => console.log('Race complete! The winner is:', val))
    .catch(err => console.log('Race ended because: ', err));

The output:
A car has crashed, don’t interrupt the race: CAR 2 CRASHED!
Race complete! The winner is: Car 1.

Awesome. This should have been the default behavior of Promise.race.

Creating the ‘perfect’ Promise.race

But we went a bit further.. What if we want a couple of cars to ‘win’, for example the first 3. instead of the first one. After all, we have three medals to hand out.

So our function needs a new parameter, to decide how many winners we need, and keep going until there are enough winners. Which gives us the following:

Promise.properRace = function(promises, count = 1) {
  promises = Array.from(promises);
  // Update our check, so it stops if there aren't enough contenders left
  if (promises.length < count) {
    return Promise.reject('Race is not finishable');
  }

  // There is no way to know which promise is resolved/rejected.
  // So we map it to a new promise to return the index.
  let indexPromises = promises.map((p, index) => p.then(() => index, () => {throw index;}));

  return Promise.race(indexPromises).then(index => {
    // The promise has resolved, remove it from the list of promises
    let p = promises.splice(index, 1)[0];
    p.then(e => console.log('Car finished:', e));
    if (count === 1) {
      // The race has finished now
      return;
    }
    // Continue the race, but now we expect one less winner because we have found one
    return Promise.properRace(promises, count-1);
  }, index => {
    // The promise has rejected, remove it from the list of promises and just
    // continue the race without changing the count.
    promises.splice(index, 1);
    return Promise.properRace(promises, count);
  });
};

let car1 = new Promise(resolve => setTimeout(resolve, 2000, 'Car 1.'));
let car2 = new Promise(resolve => setTimeout(resolve, 4000, 'Car 2.'));
let car3 = new Promise(resolve => setTimeout(resolve, 3000, 'Car 3.'));
let car4 = new Promise(resolve => setTimeout(resolve, 6000, 'Car 4.'));

Promise.properRace([car1, car2, car3, car4], 3);

The output:
Car finished: Car 1.
Car finished: Car 3.
Car finished: Car 2.

Great, that seems to work. All we need to finish up, is to actually return an array with the winning promises instead of just printing the results within the race:

Promise.properRace = function(promises, count = 1, results = []) {
  promises = Array.from(promises);
  if (promises.length < count) {
    return Promise.reject('Race is not finishable');
  }

  // There is no way to know which promise is resolved/rejected.
  // So we map it to a new promise to return the index wether it fails or succeeeds.
  let indexPromises = promises.map((p, index) => p.then(() => index, () => {throw index;}));

  return Promise.race(indexPromises).then(index => {
    // The promise has resolved, remove it from the list of promises, and add it
    // to the list of results
    let p = promises.splice(index, 1)[0];
    p.then(e => results.push(e));
    if (count === 1) {
      // The race has finished now, return the results
      return results;
    }
    // Continue the race, but now we expect one less winner because we have found one
    return Promise.properRace(promises, count-1, results);
  }, index => {
    // The promise has rejected, remove it from the list of promises and just
    // continue the race without changing the count.
    promises.splice(index, 1);
    return Promise.properRace(promises, count, results);
  });
};

let car1 = new Promise(resolve => setTimeout(resolve, 2000, 'Car 1.'));
let car2 = new Promise(resolve => setTimeout(resolve, 4000, 'Car 2.'));
let car3 = new Promise(resolve => setTimeout(resolve, 3000, 'Car 3.'));
let car4 = new Promise(resolve => setTimeout(resolve, 6000, 'Car 4.'));

Promise.properRace([car1, car2, car3, car4], 3).then(winners => {
  console.log('Race ended');
  console.log('Gold medal:', winners[0]);
  console.log('Silver medal:', winners[1]);
  console.log('Bronze medal:', winners[2]);
});

The output:
Race ended
Gold medal: Car 1.
Silver medal: Car 3.
Bronze medal: Car 2.

Perfect, let’s just test it with a car crashing, to be sure:

let car1 = new Promise((r, reject) => setTimeout(reject, 2000, 'Car 1.'));
let car2 = new Promise(resolve => setTimeout(resolve, 4000, 'Car 2.'));
let car3 = new Promise(resolve => setTimeout(resolve, 3000, 'Car 3.'));
let car4 = new Promise(resolve => setTimeout(resolve, 6000, 'Car 4.'));

Promise.properRace([car1, car2, car3, car4], 3).then(winners => {
  console.log('Race ended');
  console.log('Gold medal:', winners[0]);
  console.log('Silver medal:', winners[1]);
  console.log('Bronze medal:', winners[2]);
});

The output:
Race ended
Gold medal: Car 3.
Silver medal: Car 2.
Bronze medal: Car 4.

That works as well, looks like we created the ‘perfect’ race function.

So to conclude, be careful before using the Promise.race function, as you might not really expect this behavior. Or, you know, check the documentation first.

shadow-left