Promise.race()

This week I'm looking at the Promise.race() method, which, like Promise.all(), Promise.allSettled(), and Promise.any(), accepts an iterable of Promises. This method, however, returns a Promise that fulfills or rejects when any single Promise from the iterable settles, regardless of whether that Promise fulfilled or rejected. If that Promise from the iterable fulfills, then the returned Promise returns the value of that first Promise; if that Promise from the iterable rejects, then the returned Promise returns the reason for that first Promise's rejection.

I find it helpful to contrast this method against the others I've written about so far since they seem so similar.

Promise.all() is concerned with all of the Promises in its passed iterable fulfilling; this method will reject if any Promises in its iterable reject. It fails fast or otherwise returns an array of the values of all fulfilled Promises.

Promise.allSettled() is not concerned with fulfillment or rejection of the Promises in its passed iterable; it instead provides outcome objects for each Promise in its iterable, describing whether they fulfilled or rejected. This method does not fail fast or succeed fast, and instead always runs the length of its iterable and returns an array of the outcome objects.

Promise.any() is concerned with the fastest Promise of its iterable that fulfills; this method will reject if all Promises in its iterable reject or it is passed an empty iterable. It succeeds fast to return the value of the fulfilled Promise or otherwise returns an AggregateError that tells you why all of the Promises in its iterable failed.

Promise.race() is concerned with the fastest Promise of its iterable that settles, i.e., that either fulls or rejects. It succeeds or fails fast, returning either the value of the fulfilled Promise or the reason that the fastest Promise failed.

Syntax

Promise.race(iterable);

Parameters

  • iterable - An iterable object such as an Array.

Return Value

Promise.race() returns a pending Promise that asynchronously yields the value of the first Promise in the passed iterable to fulfill or reject.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("first promise done"), 1000);
})
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("second promise done"), 2000);
})
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("third promise done"), 500);
});
const promise4 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("fourth promise done"), 4000);
});

Promise.race([promise1, promise2, promise3, promise4]).then((value) => {
  console.log(value);
});
// expected result (after half of a second): "third promise done"

In the code above we have four Promises, all of which resolve successfully but at different times. promise3 has the shortest setTimeout(), so Promise.race() resolves with the resolved value of promise3. In this instance Promise.race() behaves exactly like Promise.any().

However, let's take a look at what happens if we make promise3 reject:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("first promise done"), 1000);
});
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("second promise done"), 2000);
});
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("reject!!"), 500));
});
const promise4 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("fourth promise done"), 4000);
});

Promise.race([promise1, promise2, promise3, promise4]).then(
  (value) => console.log(value),
  (error) => console.error(error)
);
// expected result: Error("reject!!")

In this case Promise.race() returns the Error for promise3, Error("reject!!"), since promise3 is the first Promise in the iterable to settle, even though it rejected. Had we used Promise.any() instead, it would have moved onto the next Promise to resolve, which would be promise1.

If the iterable contains one or more non-Promise values, then Promise.race() will resolve to the first of these values found in the iterable.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("first promise done"), 1000);
});
const promise2 = "second item done"; // not a Promise!
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("third promise done"), 2000);
});

Promise.any([promise1, promise2, promise3]).then((value) => {
  console.log(value);
});
// expected result (immediately):
// "second item done"

Similarly, if the iterable contains one or more already-settled Promises, then Promise.race() will resolve to the first already-settled Promise found in the iterable.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("first promise done"), 1000);
});
const promise2 = Promise.resolve("already resolved promise");
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("third promise done"), 2000);
});

Promise.race([promise1, promise2, promise3]).then((value) => {
  console.log(value);
});
// expected result (immediately):
// "already resolved promise"

Finally, if the iterable is empty, then the Promise that Promise.race() returns will be forever pending.

const uhoh = Promise.race([]).then((value) => {
  console.log(value);
});

console.log(uhoh);
// expected result: Promise { <pending> }

I admit that this last note is a puzzling one to me, considering that Promise.all() fulfills synchronously with an empty iterable and Promise.any() rejects with an empty iterable. There is an interesting discussion I found StackOverflow about this behavior from this particular method that you can read here.

From MDN: The Promise.race() method returns a Promise that fulfills or rejects as soon as one of the Promises in an iterable fulfills or rejects, with the value or reason from that Promise.

See more examples and further documentation here.