Everything about the AbortSignals (timeouts, combining signals, and how to use it with window.fetch)

Posted 2024-04-20

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
✅ copied
const 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
✅ copied
const 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
✅ copied
const [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
✅ copied
const 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
✅ copied
async 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
✅ copied
const 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

Subscribe to my
Full Stack Typescript Developer
newsletter

If you enjoyed my content and want more Full Stack Typescript content, then enter your email below.

I send a few links every couple of weeks, that I personally found useful from around the web.

Focusing on Javascript, Typescript, NextJS, and also
engineering soft skills posts.

Welcome to my site and blog - Code Driven Development.

This is where I dive topics relating to modern software engineering - with a focus on Typescript, React, web accessibility and Agile practices

I pick topics that I think are interesting, that I think others might find interesting, or write up guides that I wish I had read before learning a topic.