If you use plain old window.fetch()
to make your HTTP requests, sometimes you will want to timeout the request after a few seconds.
For the last few years I've made use of AbortController
and manually trigger an AbortSignal
.
But I learned today there are a few more advanced things you can do with these signals.
I'm going to cover:
- How to use
AbortController
(seen this in many projects - you are probably familiar) - How to use automatic
AbortSignal.timeout()
(I wasn't aware of it until today) - How to combine multiple aborts with
AbortSignal.any()
- something else I learned today
This is how you would typically use an AbortController (to get an AbortSignal) and pass it to fetch()
options so you can terminate the request:
abort-controller-demo.js✅ copiedconst controller = new AbortController(); const timeout = 5000; // 5 seconds setTimeout(() => controller.abort(`custom timeout abort`), timeout); const response = window.fetch('/your-api', { signal: controller.signal, });
This will throw if the request takes more than 5 seconds if you await
the fetch promise.
abort-controller-fetch-demo.js✅ copiedconst controller = new AbortController(); const timeout = 5000; // 5 seconds setTimeout(() => controller.abort(`custom timeout abort`), timeout); // this will throw here if it takes more than 5 seconds const response = await window.fetch('/your-api', { signal: controller.signal, });
If you catch it in a try/catch block, the error will be whatever you passed to controller.abort(...)
- in my example it is a string.
If you do not provide a value for controller.abort()
then a DOMExceptionError
is thrown (with error.name === 'AbortError'
)
Easier way to automatically terminate a fetch request with AbortSignal.timeout()
If you don't need much in terms of flexibility, and want a simple timeout after a time period (like my previous example) then there is a shortcut.
I wasn't aware of this until today (and it sounds like on twitter there are quite a few people just learning about this).
You can use AbortSignal.timeout(500)
to automatically create the abort signal which will abort after that duration (Without the boilerplate of using AbortController).
Example of how it is used:
window.fetch(apiUrlWithCacheBusting(), { signal: AbortSignal.timeout(abortTimeout), })
This will throw a DOMExceptionError
(with error.name === 'TimeoutError'
).
Combining multiple abort signals
Sometimes you might want multiple reasons for aborting a request. Luckily there is a static method on AbortSignal
to help you combine multiple signals.
Let's say you have a system where you want to abort the fetch request if:
- 30 seconds has gone
- or the user clicked a button to cancel the download
You can do this easily with two separate abort signals and combine them with AbortSignal.any()
Here is a demo:
demo.tsx✅ copiedconst [userCancelledController, setUserCancelledController] = useState<AbortController>() async function makeApiCall() { const controller = new AbortController setUserCancelledController(controller) // so we can call .abort() on it elsewhere const timeoutSignal = AbortSignal.timeout(5000); window.fetch(apiUrlWithCacheBusting(), { signal: AbortSignal.any( [ timeoutSignal, // after 5000 ms controller.signal // or if user clicks cancel button ] )}).catch(console.error) } return <> <button onClick={() => userCancelledController?.abort()}>Manual abort!</button> <button onClick={makeApiCall}> Fetch with automatic timeout abort </button> </>
Warning: this is a new feature. AbortSignal.any
has been available since Chrome 116 (August 2023) and Firefox 124 (March 2024). It isn't recommended to rely on this yet, and I was unable to find a polyfill for it.
Handling when a signal is aborted
Ok so now you can easily abort a fetch request... but we would often want to tell the user that their request was cancelled.
Luckily you can do this with event handlers. You can use an event handler on the AbortSignal to handle when it gets aborted.
(Of course, if you manually use setTimeout(() => controller.abort())
then you can handle that logic there too. This is for more advanced uses)
Here is an example, so we can update state in react when a signal is aborted. This is using .any
but you could apply this to any signal.
on-abort-event.ts✅ copiedconst userCancelledController = new AbortController const timeoutSignal = AbortSignal.timeout(abortTimeout); // this is the relevant part: const signal = AbortSignal.any([timeoutSignal, userCancelledController.signal]) signal.addEventListener('abort', () => { console.log("Aborted") }) window.fetch( '/your-api', {signal} ).catch(console.error)
One thing to be aware of is you can easily get into memory leaks if you are applying this pattern.
Checking if a signal was already aborted with throwIfAborted()
I've never used this in production code, but one thing you might find useful is throwIfAborted()
.
I think this is less useful for fetch()
requests and maybe more useful for just generally working with AbortSignal
You can use it to throw an error if a signal is already aborted. (You could probably have guessed that from the name of it). I'll show a demo:
throwIfAborted.tsx✅ copiedasync function waitForCondition(func, targetValue, { signal } = {}) { while (true) { signal?.throwIfAborted(); const result = await func(); if (result === targetValue) { return; } } }
Getting why a signal was aborted with .reason
So far all my examples have caught errors from the awaited fetch()
response, which included the abort error in the catch
block.
But if you have an AbortSignal which was aborted, you can find out what was passed to abort(...)
with signal.reason
.
abort-reason.ts✅ copiedconst controller = new AbortController(); const signal = controller.signal; if (signal.aborted) { if (signal.reason) { console.log(`Request aborted with reason: ${signal.reason}`); } else { console.log("Request aborted but no reason was given."); } } else { console.log("Request not aborted"); }
See more
- sample code showing how to use it
- AbortSignal on MDN
- hat tip to Kent C Dodds and Guilherme