neon blue coding on a black background

Implementing a Timeout in JavaScript Promises

  

Promises in JavaScript are a powerful tool for handling asynchronous operations, but there are situations where you might want to add a timeout to a promise to ensure that it doesn’t hang indefinitely. In this article, this guide will explore various techniques to add a timeout to a promise in JavaScript, ensuring code remains responsive and reliable.

JavaScript Promise Timeouts: Techniques and Best Practices

In various scenarios, developers often encounter the need to apply a timeout to a JavaScript promise. While setTimeout() may not represent the optimal solution, it can be conveniently encapsulated within 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
};

This code sample isn’t particularly complex. It essentially utilizes the Promise constructor to encapsulate setTimeout() and resolves the promise after a specified delay in milliseconds. This can prove invaluable when you need to introduce a pause in your code execution.

However, when you want to apply a timeout to another promise, this utility needs to fulfill two additional requirements. 

  • Firstly, it should allow the timeout promise to reject instead of resolving when a reason is provided as a second argument;
  • Secondly, you’ll need to create a wrapper function to seamlessly 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
red and yellow coding on a black background

As demonstrated in this example, the ‘reason’ parameter plays a crucial role in determining whether the timeout promise will resolve or reject. Subsequently, ‘awaitTimeout()’ is employed to generate a fresh promise and is combined with the original promise using ‘Promise.race()’ to implement a timeout mechanism.

However, while this implementation proves effective, it invites opportunities for enhancement across various dimensions. A notable improvement lies in the incorporation of a mechanism to clear timeouts, which involves tracking active timeout IDs. Furthermore, the demand for self-contained functionality strongly advocates for encapsulating this utility within a class structure, thereby further enhancing its modularity and versatility.

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
green coding on a black screen

 

Conclusion

This guide highlights the use of JavaScript promise timeouts to bolster application reliability and responsiveness. It starts with a basic setTimeout() encapsulation within a promise and advances to handle timeouts for external operations like API requests. Emphasizing the ‘reason’ parameter for rejecting timeout promises, it introduces a class-based approach for better code organization and timeout clearing. These techniques empower developers to create more dependable and efficient applications, improving user experiences and error handling.

Exploring JavaScript’s Asynchronous Array Iteration

   Promises in JavaScript are a powerful tool for handling asynchronous operations, but there are situations where you might want to add a timeout to a promise to ensure that it doesn’t hang indefinitely. In this article, this guide will explore various techniques to add a timeout to a promise in JavaScript, ensuring code remains …

How to Define an Enum in JavaScript: A Comprehensive Guide

   Promises in JavaScript are a powerful tool for handling asynchronous operations, but there are situations where you might want to add a timeout to a promise to ensure that it doesn’t hang indefinitely. In this article, this guide will explore various techniques to add a timeout to a promise in JavaScript, ensuring code remains …