Jay Gould

Adding a timeout to API requests with fetch

April 28, 2021

Hitting your own API service is usually predictable with the way the data is returned, especially the time it takes to get the data back. But when your website/app or server needs to hit a third party API, it might not be as predictable. If you’re hitting a busy or slow external API, your users may be waiting with a loading screen for much longer than you like.

To help address this, I like to add timeouts to my fetch requests. This ensures that the request is automatically cancelled after a certain number of seconds, and what ever happens, the user will be a response. The response can either be all of the data requested, some of the data, or an error message (depending on your requirements).

The problem: default fetch timeout of 300 seconds

If the default fetch functionality is used, your system will be waiting 300 seconds for a response before timing out:

// index.js
import * as fetch from "node-fetch"

async function getData() {
  try {
    const resp = await fetch("/get-data", {})
    const data = await resp.json()
    return data
  } catch (e) {
    // Timeout error after 300 seconds
  }
}

This is less than ideal as waiting 300 seconds is madness.

The solution: extend the default fetch

Instead, we can create our own wrapper function to use the fetch API for us:

// custom-fetch.js
import * as fetch from "node-fetch"

export default function (url: any, options: any, timeout = 5000) {
  return Promise.race([
    fetch(url, options),
    new Promise((_, reject) => setTimeout(() => reject("timeout"), timeout)),
  ])
}

The above snippet uses Promise.race(), which takes an array of promises and exits when the first one resolves or rejects. For the first element of the array we use our fetch call, passing in our own url and options params. The second element of the array is a separate promise which is set to reject after 5 seconds.

This means if 5 seconds passes and our fetch is still not resolved, the second array element promise will kick in, reject, and cause the whole fetch function to reject as a result.

The great thing about this is that we can use the fetch function in the same way as we normally would, with the option of adding in a custom timeout depending on what request we’re doing:

// index.js
import fetch from "./custom-fetch"

async function getData() {
  try {
    const resp = await fetch("/get-data", {}, 5000)
    const data = await resp.json()
    return data
  } catch (e) {
    // Timeout error after 5 seconds
  }
}

Another benefit of this is error handling:

// index.js
import fetch from "./custom-fetch"

async function getData() {
  try {
    const resp = await fetch("/get-data", {}, 5000)
    const data = await resp.json()
    return data
  } catch (e) {
    if (e === "timeout") {
      // Let the front end know the request timed out instead of throwing a generic error
    }
  }
}

Our custom-fetch.js function rejects with the timeout message, and this error is also passed back to the “catch” block of our request. This means we can send the user back a useful/accurate message rather than replying with a generic error message. You could even log the timeout to our own database for analytics reasons, or what ever will be useful for your app.

Thanks for reading!


Senior Engineer at Haven

© Jay Gould 2023, Built with love and tequila.