I’m a big fan of the 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. However it isn’t always as clear how to do things like error handling and request timeouts as it is in libraries like Axios.
If you’re not familiar with fetch
, it’s a native API that massively simplifies making AJAX requests compared to the older XHR method, and it’s supported in all modern browsers. When it initially landed, however, there was no easy way to handle request timeouts. You could fake it with Promise.race
or by wrapping your fetch
in another Promise, but these solutions don’t actually cancel the request. This is where 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, it can be used in most modern browsers and polyfills are available. 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 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:
- Create an instance of
AbortController
- Create a
setTimeout
function that calls the controller’sabort
method - Pass the controller’s
signal
tofetch
’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
.
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.
// 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.
Top comments (0)