Request Timeouts With the Fetch API
Tags: fetch, javascript
Published: 2019-12-11 22:00:19

---
title: Request Timeouts With the Fetch API
published: true
date: 2019-03-30 00:00:00 UTC
tags: fetch, javascript
canonical_url: https://www.lowmess.com/blog/fetch-with-timeout/
---

I’m a big fan of the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). I use it regularly in all sorts of projects, including my site and the API that powers the stats on the [about page](https://www.lowmess.com/about/). However it isn’t always as clear how to do things like error handling and request timeouts as it is in libraries like [Axios](https://github.com/axios/axios).

If you’re not familiar with `fetch`, it’s a native API that massively simplifies making AJAX requests compared to the older [XHR](https://developer.mozilla.org/en-US/docs/Web/API/XMLHTTPRequest) method, and [it’s supported in all modern browsers](https://caniuse.com/#feat=fetch). When it initially landed, however, there was no easy way to handle request timeouts. You could fake it with [`Promise.race`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) or by [wrapping your `fetch` in another Promise](https://github.com/github/fetch/issues/175#issuecomment-216791333), but these solutions don’t actually cancel the request. This is where [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) comes in.

`AbortController` is an API that, much like its name and my previous sentence suggests, allows us to abort (cancel) requests. Though [browser support isn’t _wonderful_ at time of writing](https://caniuse.com/#feat=abortcontroller), it can be used in most modern browsers and [polyfills are available](https://github.com/mo/abortcontroller-polyfill). The API itself has a very small surface area: a `signal` property to attach to request objects, and an `abort` method to actually cancel the request. Because the API is so simple, it’s very flexible — Jake Archibald has [a fairly in-depth article on the Google Developers blog](https://developers.google.com/web/updates/2017/09/abortable-fetch) going over various cancellation scenarios, as well as the history behind the API, and I highly recommend giving it a read.

With `AbortController`, it becomes trivial to cancel a request if it doesn’t resolve before a given period of time: if the `abort` method is called before the request resolves (or before the response `Body` is consumed), the request is cancelled; if it’s called after, the browser just ignores the call. To put it all together, we need to:

1. Create an instance of `AbortController`
2. Create a `setTimeout` function that calls the controller’s `abort` method
3. Pass the controller’s `signal` to `fetch`’s options object

## Putting It All Together

First, because we’re basically writing a shim around `fetch`, I’m going to add an extra little perk. If the response doesn’t return in the `200` range (that is, if `response.ok` evaluates to `false`), we’re going to throw an error. We absolutely do not need to do this — we could just catch our timeout and the function would work the same (we actually don’t even _need_ to do that). However, I always perform this check anyways, so this removes a lot of boilerplate code for me.

Anyways, here is my generic `fetchWithTimeout` function. It should work in any environment that supports `fetch` and `AbortController`.

```js
const fetchWithTimeout = (uri, options = {}, time = 5000) => {
  // Lets set up our `AbortController`, and create a request options object
  // that includes the controller's `signal` to pass to `fetch`.
  const controller = new AbortController()
  const config = { ...options, signal: controller.signal }

  // Set a timeout limit for the request using `setTimeout`. If the body of this
  // timeout is reached before the request is completed, it will be cancelled.
  const timeout = setTimeout(() => {
    controller.abort()
  }, time)

  return fetch(uri, config)
    .then(response => {
      // Because _any_ response is considered a success to `fetch`,
      // we need to manually check that the response is in the 200 range.
      // This is typically how I handle that.
      if (!response.ok) {
        throw new Error(`${response.status}: ${response.statusText}`)
      }

      return response
    })
    .catch(error => {
      // When we abort our `fetch`, the controller conveniently throws a named
      // error, allowing us to handle them separately from other errors.
      if (error.name === 'AbortError') {
        throw new Error('Response timed out')
      }

      throw new Error(error.message)
    })
}
```

Using the function is fairly straightforward. Because we return `fetch` directly, we can use it in much the same way; the only change should be the addition of a third parameter (our `time` argument) and the extra error handling we discussed above.

```js
// This example _always_ logs the error, because I'm telling httpstat.us to wait
// at least 1s before responding, but setting the timeout threshold to 500ms.
// Also, this could definitely be written in async/await if you preferred.
fetchWithTimeout(
  'https://httpstat.us/200?sleep=1000',
  { headers: { Accept: 'application/json' } },
  500
)
  .then(response => response.json())
  .then(json => {
    console.log(`This will never log out: ${json}`)
  })
  .catch(error => {
    console.error(error.message)
  })
```

* * *

That’s it. That’s the whole post. Though the snippet is ultimately pretty simple (it’s 20 lines without whitespace and comments) writing this provided me with three major benefits: it forced me to abstract the function to the most reusable version I could, it gave me an opportunity to research `AbortController` to make sure I knew exactly how it behaved, and it provided a place where I can come find this snippet in the future instead of rooting through old projects.