cancel fetch requests with abortcontroller

2020-08-15

 | 

~4 min read

 | 

656 words

Update: AbortController is now shipping in Node, though currently under an experimental flag. The API is based on the web API described below. This is exciting as it brings the same capability to cancel Promises to the server!

With the introduction of the AbortController, we now have the ability to cancel fetch requests declaratively. This allows an early escape from a Promise which does not have its own method for canceling (i.e. there’s no Promise.cancel() to abort).

To make use of this, we’ll need a few pieces:

  1. An AbortController instance
  2. Attach the instance’s signal property to the cancelable event
  3. Trigger an abort with the instance method

A bare bones example might look like the following. Let’s imagine we want to get information about Pikachu from the pokeapi:

basicFetch.js
fetch("https://pokeapi.co/api/v2/pokemon/pikachu", { signal }).then((res) =>
  console.log(res),
)

But then, we decide we actually want to abort that request:

canceledFetch.js
const controller = new AbortController()
const { signal } = controller

fetch("https://pokeapi.co/api/v2/pokemon/pikachu", { signal })
  .then((res) => console.log(res))
  .catch((e) => console.log({ e, name: e.name }))

controller.abort()

The resulting error’s name is AbortError with a message of "The user aborted a request."

This works because we’ve passed the signal into the fetch which acts as a listener for anytime an instance of the AbortController executes its abort method. The signal is an AbortSignal, an object that facilitates communication with a DOM request.

What about adding more control? Instead of a blanket abort, we can abort the fetch if too much time has passed using setTimeout to manage the execution. For example:

cancelableAfterTimeoutFetch.js
const fetchHandler = async (url, options) => {
  const controller = new AbortController()
  const { signal } = controller
  const timeout = setTimeout(() => {
    controller.abort()
  }, 500)

  try {
    return await fetch(url, {
      ...options,
      signal,
    })
  } catch (error) {
    if (error.name === "AbortError") {
      throw new Error(`Fetch canceled due to timeout`)
    } else {
      throw new Error(`Generic Error: ${error}`)
    }
  } finally {
    clearTimeout(timeout)
  }
}

This example is technically more verbose than it needs to be, but I like it for its explicitness.1 This also builds on the same principles we saw earlier, except now the abort method is only called if 500 milliseconds elapse before we get to the finally block. If we get there then the timeout is cleared and the abort method will never execute.

Other Ways To Cancel Requests

While the AbortController brought the ability to cancel to the fetch API, we’ve had the ability cancel XHR requests for a while. The XMLHttpRequest has an abort method:

cancelXHR.js
// source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort
var xhr = new XMLHttpRequest(),
  method = "GET",
  url = "https://developer.mozilla.org/"
xhr.open(method, url, true)

xhr.send()

if (OH_NOES_WE_NEED_TO_CANCEL_RIGHT_NOW_OR_ELSE) {
  xhr.abort()
}

Additionally libraries that facilitate requests, like Axios, have made canceling ergonomic for a while, offering two solutions:

  1. Canceling with a cancelToken

    cancelAxiosWithToken.js
    // source: https://github.com/axios/axios#cancellation
    const CancelToken = axios.CancelToken
    const source = CancelToken.source()
    
    axios
     .get("/user/12345", {
       cancelToken: source.token,
     })
     .catch(function (thrown) {
       if (axios.isCancel(thrown)) {
         console.log("Request canceled", thrown.message)
       } else {
         // handle error
       }
     })
    
    axios.post(
     "/user/12345",
     {
       name: "new name",
     },
     {
       cancelToken: source.token,
     },
    )
    
    // cancel the request (the message parameter is optional)
    source.cancel("Operation canceled by the user.")
  2. Cancel by passing an executor function to a CancelToken constructor

    cancelAxiosWIthConstructor.js
    // source: https://github.com/axios/axios#cancellation
    const CancelToken = axios.CancelToken
    let cancel
    
    axios.get("/user/12345", {
     cancelToken: new CancelToken(function executor(c) {
       // An executor function receives a cancel function as a parameter
       cancel = c
     }),
    })
    
    // cancel the request
    cancel()

Note of Appreciation

Thank you to Jake Archibald, David Walsh, and MDN for their work in making cancelable fetch requests a reality (in the case of Jake) and understandable (all).

Footnotes

  • 1 It is not strictly necessary to clear the timeout, however, because as Jake Archibald notes:

    It’s ok to call .abort() after the fetch has already completed, fetch simply ignores it.


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!