logoMiyauchi

Aborting fetch request with AbortController

Introduction

AbortController is an interface for aborting asynchronous processes, and has been available in Node.js since 15.0.0. In this article, we'll explain how to cancel an HTTP request, a typical asynchronous process.

In the past, we used XMLHttpRequest to send HTTP requests, but nowadays we almost use the Promise-based Fetch API.

HTTP client libraries such as axios and ky are highly used, the basic Fetch API cancellation is explained here.

About Fetch API

The Fetch API is available as standard in modern browsers and Deno. Even Node.js has node-fetch, so it's no exaggeration to say that the Fetch API is universally available as an HTTP client. So, first of all, let's make sure you know how to use it with the Fetch API.

The Fetch API takes an object called RequestInit as its second argument. The interface is as follows:

declare function fetch(
  input: Request | URL | string,
  init?: RequestInit
): Promise<Response>

interface RequestInit {
  body?: BodyInit | null
  cache?: RequestCache
  credentials?: RequestCredentials
  headers?: HeadersInit
  integrity?: string
  keepalive?: boolean
  method?: string
  mode?: RequestMode
  redirect?: RequestRedirect
  referrer?: string
  referrerPolicy?: ReferrerPolicy
  signal?: AbortSignal | null
  window?: any
}

The key signal takes an AbortSignal.AbortSignal is a member of class AbortController.

About AbortController

AbortController is a controller that contains a signal object that can abort an asynchronous process. You can create an object from the constructor.

const controller = new AbortController()

declare class AbortController {
  readonly signal: AbortSignal
  abort(): void
}

const controller = new AbortController() declare class AbortController { readonly signal: AbortSignal abort(): void}

The AbortController has a reference to the signal object and an abort method. You can abort an HTTP request by passing this signal to fetch and calling the abort method.

The follow example assumes a non-Deno execution environment. Deno does not yet implement cancellation of the Fetch API as of 1.10.3. It has been merged into the main branch and will probably be available soon.

Top-Level Await notation is used.

const url = 'https://google.com'const controller = new AbortController() await fetch(url, { signal: controller.signal}) setTimeout(() => { controller.abort()}, 1000)

In the above example, the request will be aborted after 1000 milliseconds. In the UI, user-initiated cancelling can be achieved by binding a call to the abort function to a button click event, for example.

Now that we have aborted, let's think about what to do after the abort.

There are several ways to handle abort. Let's look at each of them.

Rejecting the Fetch API

In the Fetch API, reject is defined to occur in the following two cases. See specification for details.

  • TypeError
  • AbortError

TypeError is thrown when a network error occurs. For example, a request for a non-existent URL will raise a TypeError.

await fetch('https://this-is-not-exist.com')Uncaught TypeError: error sending request for url (https://this-is-not-exist.com/): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known

And another error is AbortError. This is raised when the request is aborted.

By picking up AbortError, we can handle the error exactly as it should be handled. Also, by picking up TypeError and AbortError, you can make notifications more user-friendly.

try { 
  await fetch('https://this-is-not-exist.com')
} catch (e) { 
  if (e.name === 'AbortError') { 
    // Abort error handling 
  } else {
    // Network error handling 
  }
}

In the above example, I used the tryCatch statement to catch errors, but of course you can also pick up errors from the reject function of Promise.

Event Handlers and Event Listeners

The interface of AbortSignal is as follows:

interface AbortSignal extends EventTarget { readonly aborted: boolean onabort: ((this: AbortSignal, ev: Event) => any) | null addEventListener<K extends keyof AbortSignalEventMap>( type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | AddEventListenerOptions ): void removeEventListener<K extends keyof AbortSignalEventMap>( type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | EventListenerOptions ): void}

AbortSignal has an event handler called onabort. By setting any function to it, the function will be called on abort.

const controller = new AbortController();
controller.signal.onabort = () => {}

You can also monitor for abort event in the same way by setting the type of the event listener to abort.

controller.signal.addEventListener('abort', () => {})

Also, the read-only property aborted indicates whether the AbortSignal has been aborted or not.

Abort multiple HTTP requests

The AboutController can be passed to multiple calls to the fetch function to abort a batch HTTP requests.

const controller = new AbortController()
const { signal } = controller 
try { 
  await Promise.all(
    [endpoint1, endpoint2, endpoint3].map((url) => { 
      fetch(url, { signal }) 
    }) 
  )
} catch (e) {}

You can also catch errors in bulk.

Abort multiple times

Once an AbortController calls abort, it can't run the fetch function again that references that AbortSignal.

For example, in Vue, you might end up writing something like this.

<script setup lang="ts"> 
  const controller = new AbortController()  
  const onCancel = () => { controller.abort() }  
  const onClick = async () => { 
    await fetch(url, { signal: controller.signal }) 
  }
</script>

In this example, the AbortController instance is not regenerated on every onClick, so you cannot make a second HTTP request. Since you need to make the instance for each fetch, you do so as follows:

<script setup lang="ts"> 
  let controller  
  
  const onCancel = () => { controller?.abort() }  
  const onClick = async () => { 
    controller = new AbortController() 
    await fetch(url, { signal: controller.signal }) 
  }
</script>

Unfortunately, due to the scope of the variable, you have to declare it as let, but now you can set a new instance every time you fetch.