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 = [];
}
set = (delay, reason) =>
new Promise((resolve, reject) => {
const id = setTimeout(() => {
if (reason === undefined) resolve();
else reject(reason);
this.clear(id);
}, delay);
this.ids.push(id);
});
wrap = (promise, delay, reason) =>
Promise.race([promise, this.set(delay, reason)]);
clear = (...ids) => {
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();
timeout.set(6000).then(() => console.log('Hello'));
timeout2.set(4000).then(() => console.log('Hi'));
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