Many times in the past I’ve found myself needing to add a timeout to a
promise in JavaScript. setTimeout()
is not exactly a perfect
tool for the job, but it’s easy enough to wrap it into a promise:
const awaitTimeout = delay =>
new Promise(resolve => setTimeout(resolve, delay));
awaitTimeout(300).then(() => console.log('Hi'));
// Logs 'Hi' after 300ms
const f = async () => {
await awaitTimeout(300);
console.log('Hi'); // Logs 'Hi' after 300ms
; }
There’s nothing particularly complicated about this code sample, really.
All it does is use the Promise
constructor to wrap
setTimeout()
and resolve the promise after
delay
ms. This can be a useful tool when some code has to
stall for a given amount of time.
In order to add a timeout to another promise, however, there are two additional needs this utility has to satisfy. The first one is allowing the timeout promise to reject instead of resolving when provided a reason as a second argument. The other one is to create a wrapper function which will add the timeout to the promise:
const awaitTimeout = (delay, reason) =>
new Promise((resolve, reject) =>
setTimeout(
=> (reason === undefined ? resolve() : reject(reason)),
()
delay
);
)
const wrapPromise = (promise, delay, reason) =>
Promise.race([promise, awaitTimeout(delay, reason)]);
wrapPromise(fetch('https://cool.api.io/data.json'), 3000, {
reason: 'Fetch timeout',
}).then(data => {
console.log(data.message);
}).catch(data => console.log(`Failed with reason: ${data.reason}`));
// Will either log the `message` if `fetch` completes in under 3000ms
// or log an error message with the reason 'Fetch timeout' otherwise
As you can see in this example, reason
is used to determine
if the timeout promise will resolve or reject.
awaitTimeout()
is then used to create a new promise and
passed to Promise.race()
along with the other promise to
create a timeout.
This implementation definitely works, but we can take it a couple steps
further. An obvious improvement is the addition of a way to clear a
timeout, which requires storing the ids of any active timeouts. This,
along with the need to make this utility self-contained both make a great
case for using a class
:
class Timeout {
constructor() {
this.ids = [];
}
= (delay, reason) =>
set new Promise((resolve, reject) => {
const id = setTimeout(() => {
if (reason === undefined) resolve();
else reject(reason);
this.clear(id);
, delay);
}this.ids.push(id);
;
})
= (promise, delay, reason) =>
wrap Promise.race([promise, this.set(delay, reason)]);
= (...ids) => {
clear this.ids = this.ids.filter(id => {
if (ids.includes(id)) {
clearTimeout(id);
return false;
}return true;
;
});
}
}
const myFunc = async () => {
const timeout = new Timeout();
const timeout2 = new Timeout();
.set(6000).then(() => console.log('Hello'));
timeout.set(4000).then(() => console.log('Hi'));
timeout2
timeout.wrap(fetch('https://cool.api.io/data.json'), 3000, {
reason: 'Fetch timeout',
}).then(data => {
console.log(data.message);
}).catch(data => console.log(`Failed with reason: ${data.reason}`))
.finally(() => timeout.clear(...timeout.ids));
;
}// Will either log the `message` or log a 'Fetch timeout' error after 3000ms
// The 6000ms timeout will be cleared before firing, so 'Hello' won't be logged
// The 4000ms timeout will not be cleared, so 'Hi' will be logged