diff --git a/blog_images/walking.jpg b/blog_images/walking.jpg new file mode 100644 index 000000000..92d722ee4 Binary files /dev/null and b/blog_images/walking.jpg differ diff --git a/blog_posts/javascript-await-timeout.md b/blog_posts/javascript-await-timeout.md new file mode 100644 index 000000000..a6f444ad7 --- /dev/null +++ b/blog_posts/javascript-await-timeout.md @@ -0,0 +1,106 @@ +--- +title: How can I add a timeout to a promise in JavaScript? +type: question +tags: javascript,promise,timeout +authors: chalarangelo +cover: blog_images/walking.jpg +excerpt: Oftentimes you might need to add a timeout to a promise in JavaScript. Learn how to do this and more in this short guide. +--- + +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: + +```js +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: + +```js +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`: + +```js +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 +``` + +**Image credit:** [Sandro Antonietti](https://unsplash.com/@s_antonietti?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)