AbortController: how to cancel a fetch request in JavaScript

By

Learn how to cancel fetch requests in JavaScript with AbortController, handle AbortError, set timeouts, and abort on unmount or superseded requests.

~~~

AbortController lets you cancel a fetch request before it finishes. Without it, a slow network call keeps running even after the user navigates away.

The controller exposes a signal. You pass that signal to fetch(). Call abort() on the controller and the request stops.

Cancel a fetch

Create a controller, pass signal to fetch, and abort when you need to stop.

const controller = new AbortController()

fetch('/api/users', { signal: controller.signal })
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((err) => {
    if (err.name === 'AbortError') {
      console.log('Request was cancelled')
      return
    }
    console.error(err)
  })

controller.abort()

The fetch promise rejects with an AbortError. Always check err.name so you do not treat a cancel like a real failure.

Timeout with AbortSignal.timeout()

You do not need to wire a timer yourself anymore. AbortSignal.timeout() aborts after a set number of milliseconds.

fetch('/api/users', { signal: AbortSignal.timeout(5000) })
  .then((response) => response.json())
  .catch((err) => {
    if (err.name === 'TimeoutError') {
      console.log('Request timed out')
    }
  })

Five seconds pass and the browser cancels the request for you.

Abort on component unmount

In a UI framework, you often fetch data when a component mounts. If the user leaves before the response arrives, you should cancel.

const controller = new AbortController()

async function loadPosts() {
  try {
    const response = await fetch('/api/posts', {
      signal: controller.signal
    })
    const posts = await response.json()
    renderPosts(posts)
  } catch (err) {
    if (err.name === 'AbortError') return
    showError(err)
  }
}

loadPosts()

// call this when the component unmounts
function cleanup() {
  controller.abort()
}

Without cleanup, the old request might update state on a component that no longer exists.

Superseded requests

Search boxes are a classic case. The user types fast and you fire a fetch on every keystroke. Only the latest result should win.

Keep one controller in scope. Abort the previous request before starting a new one.

let controller

async function search(query) {
  controller?.abort()
  controller = new AbortController()

  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(query)}`,
      { signal: controller.signal }
    )

    return response.json()
  } catch (err) {
    if (err.name === 'AbortError') return
    throw err
  }
}

The optional chaining on controller?.abort() avoids an error on the first call when nothing exists yet. The aborted request returns undefined; only the latest completed request returns results.

AbortSignal.any()

Sometimes you want to abort when any of several signals fire. AbortSignal.any() combines them.

const userCancel = new AbortController()
const timeout = AbortSignal.timeout(8000)

const signal = AbortSignal.any([
  userCancel.signal,
  timeout
])

fetch('/api/report', { signal })
  .then((response) => response.json())
  .then((data) => console.log(data))

The fetch stops if the user clicks cancel or if eight seconds pass, whichever comes first.

How I used it in Port Pilot

I used AbortController in Port Pilot, a tool I built to inspect local services running on my Mac.

Port Pilot probes local ports to show the HTTP status, page title, and response time. Some ports never return an HTTP response, so every probe gets 450 milliseconds to finish:

async function probe(url) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), 450)

  try {
    return await fetch(url, { signal: controller.signal })
  } catch {
    return null
  } finally {
    clearTimeout(timer)
  }
}

Without the abort, one unresponsive service could slow down the whole scan. With it, Port Pilot can move on and keep the dashboard fast.

My advice is to use AbortController whenever a fetch can outlive the work that started it, or when you need a hard time limit. It keeps your app responsive and avoids race conditions.

Tagged: JavaScript ยท All topics
~~~

Related posts about js: