A promise has two states: either pending or settled (resolved or rejected). The user has no control over the time it takes from going from the first state to the second. Which makes it harder to bail out on a certain promise when it takes too long in a promise friendly way.

Promise.race() to the rescue.

Table of contents

How does Promise.race work?

This method takes an array of promises and - as its name suggests - races them, the first one to be settled in either state wins.

Example:

const resolveAfter = (duration, value) =>
  new Promise((resolve, reject) => setTimeout(() => resolve(value), duration));

let first = resolveAfter(100, "value from first");
let second = resolveAfter(200, "value from second");

Promise.race([first, second]).then(console.log);
// logs 'value from first'

And it works with errors too as you might expect:

const resolveAfter = (duration, value) =>
  new Promise((resolve, reject) => setTimeout(() => resolve(value), duration));
const rejectAfter = (duration, err) =>
  new Promise((resolve, reject) => setTimeout(() => reject(err), duration));

let first = rejectAfter(100, new Error("oops in first"));
let second = resolveAfter(200, "value from second");

Promise.race([first, second]).then(console.log).catch(console.error);
// logs: 'Error: oops in first'

Leverage Promise.race to race promises against the clock

The first ingredient is a promise that resolves after a timeout. We have already seen that in the previous example.

The second is a specific Error class to be sure it came from the rejected timeout and not the original promise we were awaiting. We could implement a specific class that extends Error like this:

class TimeoutError extends Error {
  constructor(...args) {
    super(...args);
  }
}

const resolveAfter = (duration, value) =>
  new Promise((resolve, reject) => setTimeout(() => resolve(value), duration));
const rejectAfter = (duration, err) =>
  new Promise((resolve, reject) => setTimeout(() => reject(err), duration));

let first = rejectAfter(100, new TimeoutError("Timeout!"));
let second = resolveAfter(200, "value from second");

Promise.race([first, second])
  .then(console.log)
  .catch(err => {
    if (err instanceof TimeoutError) {
      // handleTimeoutError(err)
    } else {
      // handleOtherError(err)
    }
    console.error(err);
  });

// logs: Error: Timeout!

You could imagine moving this logic to its own module and abstracting away the timeout logic like this:

// module: timeout.js

const rejectAfter = (duration, err) =>
  new Promise((resolve, reject) => setTimeout(() => reject(err), duration));

export class TimeoutError extends Error {
  constructor(...args) {
    super(...args);
  }
}
export const withTimeout = (promise, timeout = 0) => {
  return Promise.race([
    promise,
    rejectAfter(100, new TimeoutError("Timeout!")),
  ]);
};

// module: user.js

import { withTimeout, TimeoutError } from "./timeout";

const resolveAfter = (duration, value) =>
  new Promise((resolve, reject) => setTimeout(() => resolve(value), duration));

withTimeout(resolveAfter(200, "value from my promise"), 100)
  .then(console.log)
  .catch(console.error);
// logs: Error: Timeout!
withTimeout(resolveAfter(100, "value from my promise"), 200)
  .then(console.log)
  .catch(console.error);
// logs: value from my promise

Conclusion

I hope you have found this short article helpful. Promise.race() doesn’t get a lot of love, but we leveraged it to solve a common question amongst promise users.