Promises: run any promise with a timeout
abdellah ht
Posted on March 18, 2022
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 🤗
Posted on March 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.