abdellah hariti
Abdellah H.

Abdellah H.

Promises: run any promise with a timeout

Promises: run any promise with a timeout

abdellah hariti's photo
abdellah hariti
·Mar 18, 2022·

3 min read

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.

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 fisrst 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 timout 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 commnon question amongst promise users.

If you have any remarks or questions, please leave them in the comments. I will be happy to answer each one of them.

And don't forget to follow for more 🤗

 
Share this