---
title: How to Cancel a Fetch Request in JavaScript (2026)
description: 'Cancel a fetch request in JavaScript and React with AbortController: abort on unmount, add a timeout with AbortSignal.timeout, and handle AbortError safely.'
url: https://nerdleveltech.com/cancel-fetch-request-abortcontroller-react
language: en
date: '2026-06-29'
category: frontend
tags:
  - abortcontroller
  - fetch api
  - javascript
  - react
  - abortsignal
  - async
  - frontend
markdown_url: https://nerdleveltech.com/cancel-fetch-request-abortcontroller-react.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/cancel-fetch-request-abortcontroller-react'
terms: https://nerdleveltech.com/llms.txt
---

# How to Cancel a Fetch Request in JavaScript (2026)

To cancel a fetch request, create an `AbortController`, pass its `signal` to `fetch(url, { signal })`, and call `controller.abort()` when you want to stop. The request's promise rejects with an `AbortError` you can safely ignore. In React, call `abort()` from your `useEffect` cleanup.

## TL;DR

Canceling a fetch comes down to one web-standard tool: **`AbortController`**. Create a controller, pass `controller.signal` to `fetch`, and call `controller.abort()` to stop the request — it then rejects with a `DOMException` named **`AbortError`**. For timeouts, skip the old `setTimeout` dance and use **`AbortSignal.timeout(ms)`**, which rejects with a **`TimeoutError`**; combine a timeout with a cancel button using **`AbortSignal.any([...])`**. In React, create the controller inside `useEffect` and call `abort()` in the cleanup to kill stale requests and avoid race conditions. The core AbortController, timeout, and `AbortSignal.any()` behaviors shown here were verified against a live server on Node.js 22.

## What you'll learn

- How to cancel a fetch request with `AbortController` and a `signal`
- How to detect and safely ignore the `AbortError` a cancel throws
- How to cancel a fetch on unmount (and avoid race conditions) in a React `useEffect`
- Why your fetch fires twice in development, and when to worry about it
- How to add a request timeout with `AbortSignal.timeout()`
- How to combine a timeout and a cancel button with `AbortSignal.any()`
- Why an `AbortController` is single-use, and how to reuse the pattern correctly
- Which browsers and runtimes support these APIs
- How axios and TanStack Query handle cancellation

## How do you cancel a fetch request in JavaScript?

Create an `AbortController`, pass its `signal` to `fetch`, and call `abort()` when you want to stop the request. `AbortController` is the standard, built-in way to make a `fetch` cancellable — no library required.[^mdn-abortsignal]

```js
// Create a controller and grab its signal
const controller = new AbortController();

fetch("/api/search?q=react", { signal: controller.signal })
  .then((res) => res.json())
  .then((data) => console.log(data))
  .catch((err) => {
    if (err.name === "AbortError") {
      console.log("Request was cancelled");
    } else {
      throw err; // a real network or parsing error
    }
  });

// Later — cancel it
controller.abort();
```

When `abort()` is called, the `fetch()` promise rejects with a `DOMException` named `AbortError`.[^mdn-abortsignal] The `signal` is the read-only side of the controller that you hand to `fetch`; the `controller` keeps the `abort()` button. You can pass the *same* signal to several `fetch` calls, and a single `abort()` cancels all of them at once — useful when one user action kicks off a batch of requests.

## How do you handle the AbortError after canceling a fetch?

Catch the rejection and check `err.name === "AbortError"`. A cancel is something *you* triggered on purpose, so the usual move is to ignore that specific error and re-throw everything else. The `async/await` version reads cleanly:

```js
const controller = new AbortController();

try {
  const res = await fetch(url, { signal: controller.signal });
  const data = await res.json();
  // use data
} catch (err) {
  if (err.name === "AbortError") return; // expected — do nothing
  throw err; // anything else is a genuine failure
}
```

If you want to know *why* a request was cancelled, pass a reason to `abort()`. Whatever value you pass becomes the rejection reason and is also stored on `signal.reason`:

```js
controller.abort(new Error("User navigated away"));
```

Calling `controller.abort(new Error("User navigated away"))` makes the fetch reject with that exact `Error` object — verified by identity in testing — instead of the default `AbortError`.[^mdn-abortsignal] You can also guard code that should not run once a signal is aborted with `signal.throwIfAborted()`, which throws the stored reason if the signal has already aborted and does nothing otherwise.[^mdn-abortsignal]

## How do you cancel a fetch request in React with useEffect?

Create the `AbortController` inside the effect, pass its `signal` to `fetch`, and return a cleanup function that calls `controller.abort()`. React runs that cleanup when the component unmounts *and* before the effect re-runs, so a stale request is cancelled before a new one starts.[^react-effects]

```jsx
import { useEffect, useState } from "react";

function SearchResults({ query }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function load() {
      try {
        const res = await fetch(`/api/search?q=${query}`, {
          signal: controller.signal,
        });
        setData(await res.json());
      } catch (err) {
        if (err.name !== "AbortError") throw err;
      }
    }
    load();

    return () => controller.abort(); // cancel on unmount or when query changes
  }, [query]);

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```

This solves two real bugs. The first is a **race condition**: if `query` changes from `"alice"` to `"bob"`, the slower `"alice"` response could land last and overwrite `"bob"`. Aborting the old request makes that impossible. The second is the classic "can't update state on an unmounted component" pattern, because the cancelled request never reaches `setData`.

The React docs offer a second pattern — an `ignore` flag — that you can use instead of, or alongside, aborting:[^react-effects]

```jsx
useEffect(() => {
  let ignore = false;
  fetch(`/api/search?q=${query}`)
    .then((res) => res.json())
    .then((data) => {
      if (!ignore) setData(data);
    });
  return () => {
    ignore = true;
  };
}, [query]);
```

The difference is concrete: the `ignore` flag *discards* a stale response but the network request still completes, while `AbortController` actually *cancels* the request and saves the bandwidth. Aborting is the stronger choice when responses are large or expensive.

## Why does my fetch run twice or abort immediately in React?

Because of `<StrictMode>` in development. React intentionally double-invokes effects in development — setup, then cleanup, then setup again — to help you catch missing cleanup.[^react-strict] With an `AbortController` cleanup in place, that means your first request is aborted almost immediately, so you may see an extra request and an `AbortError` (or a "canceled" entry in the Network tab) while developing.

This is expected and **development-only**. In production the effect runs once and nothing is aborted prematurely. The fact that you see the cancel proves your cleanup works — it is not a bug to "fix" by deleting `StrictMode` or the cleanup function. Because the example above already ignores `AbortError`, the doubled request causes no visible problem for users.

## How do you add a timeout to a fetch request?

Pass `AbortSignal.timeout(ms)` as the `signal`. The Fetch standard has no built-in `timeout` option, so this static method is the modern, official way to give a request a deadline.[^fetch-spec] The signal aborts automatically after the given number of milliseconds and rejects the fetch with a `TimeoutError` `DOMException`.[^mdn-timeout]

```js
try {
  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
  const data = await res.json();
  // use data
} catch (err) {
  if (err.name === "TimeoutError") {
    console.error("Request timed out after 5 seconds");
  } else if (err.name === "AbortError") {
    console.error("Request was cancelled by the user");
  } else {
    throw err; // a network or other error
  }
}
```

The key detail is the error name. A timeout rejects with `TimeoutError`, while a user-triggered abort rejects with `AbortError`, so a single `catch` can tell the two apart — verified by running a 50 ms timeout against a one-second endpoint.[^mdn-timeout] That distinction matters: a timeout usually deserves a "try again" message, while a deliberate cancel usually deserves silence. (One historical caveat: Chromium-based browsers before Chrome 124 reported a timed-out fetch as an `AbortError` rather than a `TimeoutError`; current browsers follow the spec.[^chrome-fix]) This replaces the older pattern of creating an `AbortController`, starting a `setTimeout` that calls `abort()`, and clearing the timer by hand.

## How do you combine a timeout with a cancel button?

Use `AbortSignal.any()` to merge several signals into one. It takes an iterable of signals and returns a signal that aborts as soon as *any* of them aborts — perfect for "cancel after 10 seconds **or** when the user clicks Cancel."[^mdn-any]

```js
const controller = new AbortController();
cancelButton.addEventListener("click", () => controller.abort());

const res = await fetch(url, {
  signal: AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]),
});
```

The combined signal adopts the reason of whichever source signal aborts first.[^mdn-any] One caveat is worth knowing: MDN notes that, unlike a bare `AbortSignal.timeout()`, you cannot always rely on the error name alone to tell whether a *combined* abort came from the timeout.[^mdn-abortsignal] When you need the cause for certain, check which controller fired instead of the error name:

```js
} catch (err) {
  if (controller.signal.aborted) {
    console.log("Cancelled by the user");
  } else {
    console.log("Request timed out");
  }
}
```

`controller.signal.aborted` is `true` only when your own cancel handler ran, so it separates a manual cancel from a timeout reliably across engines. Either way, you get layered cancellation — timeout *and* manual — from a single `signal`, without juggling timers yourself.

## Can you reuse an AbortController for multiple requests?

No — an `AbortController` (and its signal) is single-use. As MDN puts it, a signal "can only be used once"; after it aborts, any fetch using that same signal is rejected immediately.[^mdn-abortsignal] In testing, handing an already-aborted signal to a fresh `fetch` rejected it instantly with `AbortError`.

So the rule is: **create a new `AbortController` for each logical operation you might cancel.** What a single controller *can* do is cover several requests you want to cancel *together* — pass one signal to multiple `fetch` calls and a single `abort()` stops them all. If you need to cancel them independently, give each its own controller.

```js
// One controller per request you might cancel on its own
function startRequest(url) {
  const controller = new AbortController();
  const promise = fetch(url, { signal: controller.signal });
  return { promise, cancel: () => controller.abort() };
}

const { promise, cancel } = startRequest("/api/report");
// cancel(); // stops just this request
```

## Is AbortController supported in all browsers and Node.js?

`AbortController`, `AbortSignal`, and abortable `fetch` are Baseline "widely available" features; MDN lists `AbortController` as available across browsers since March 2019, so the core cancel pattern is safe to use everywhere today.[^mdn-abortcontroller] The two newer helpers are more recent: MDN lists `AbortSignal.timeout()` as Baseline 2024 (available since April 2024) and `AbortSignal.any()` as Baseline 2024 (available since March 2024).[^mdn-timeout][^mdn-any]

If you must support older browsers that lack `AbortSignal.timeout()`, the classic fallback still works: create an `AbortController`, start a `setTimeout` that calls `controller.abort()`, and clear the timer once the request resolves. These APIs also exist in modern server runtimes — Node.js, Deno, and Bun — so the same cancellation code runs on the server.

## Do axios and TanStack Query cancel requests the same way?

Yes — both build on the same `AbortController` standard, so the concept carries over directly. With **axios**, you pass an `AbortController` signal in the request config; its legacy `CancelToken` API was deprecated back in v0.22.0 (October 2021) and you should prefer the signal approach.[^axios]

```js
const controller = new AbortController();
axios.get("/api/search", { signal: controller.signal });
controller.abort();
```

With **TanStack Query**, you usually do not create a controller at all: the library passes an `AbortSignal` into your query function and aborts it automatically when a query becomes stale or inactive. You just forward that signal to `fetch`.[^tanstack]

```js
useQuery({
  queryKey: ["todos"],
  queryFn: async ({ signal }) => {
    const res = await fetch("/api/todos", { signal });
    return res.json();
  },
});
```

By default, TanStack Query does not cancel a request that becomes unused before it resolves unless you actually consume that `signal`, so wiring it into your fetch is what unlocks automatic cancellation.[^tanstack]

## FAQ

**How do you cancel a fetch request in JavaScript?**
Create an `AbortController`, pass `controller.signal` to `fetch(url, { signal })`, and call `controller.abort()` to cancel. The fetch promise then rejects with a `DOMException` named `AbortError`.[^mdn-abortsignal]

**How do I stop the AbortError from showing up as an error?**
Catch it and check the name: `if (err.name === "AbortError") return;`. A cancel is intentional, so ignore that one error and re-throw the rest.[^mdn-abortsignal]

**How do I cancel a fetch when a React component unmounts?**
Create the controller inside `useEffect`, pass its signal to `fetch`, and return `() => controller.abort()` as the cleanup. React calls it on unmount and before the effect re-runs.[^react-effects]

**Why does my fetch run twice in development?**
React's `<StrictMode>` double-invokes effects in development to surface missing cleanup, so your request runs, gets aborted, and runs again. It is development-only and does not happen in production.[^react-strict]

**How do I add a timeout to fetch?**
Pass `AbortSignal.timeout(ms)` as the signal. `fetch` has no built-in timeout option, and a timeout rejects with a `TimeoutError`.[^mdn-timeout][^fetch-spec]

**Can I combine a timeout and a manual cancel?**
Yes. Use `AbortSignal.any([controller.signal, AbortSignal.timeout(ms)])`; the request aborts when either fires, adopting the reason of whichever came first.[^mdn-any]

**Can I reuse an AbortController after calling abort()?**
No. A signal is single-use — once aborted, any fetch using it rejects immediately, so create a new controller per cancellable operation.[^mdn-abortsignal]

## Bottom line

Canceling a fetch is a one-tool job: `AbortController` for manual cancellation, `AbortSignal.timeout()` for deadlines, and `AbortSignal.any()` to combine them — all standard, all built in, all supported across modern browsers and runtimes. In React, the whole pattern collapses to "make a controller in `useEffect`, abort it in the cleanup," which kills stale requests and the race conditions that come with them.

To go deeper on the async foundations underneath all of this, see [how callbacks, promises, and async/await work](https://nerdleveltech.com/understand-the-asynchronous-javascript-callbacks-promises-and-async-await) and our guide to [modern JavaScript async patterns](https://nerdleveltech.com/mastering-javascript-async-patterns-from-callbacks-to-concurrency). And if you would rather let a library handle cancellation, caching, and race conditions for you, see how [TanStack Query manages requests and optimistic updates](https://nerdleveltech.com/tanstack-query-optimistic-updates-rollback-tutorial).

[^mdn-abortsignal]: MDN Web Docs — AbortSignal interface (abort behavior, single-use signals, `reason`, `throwIfAborted`). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
[^mdn-timeout]: MDN Web Docs — AbortSignal: timeout() static method (TimeoutError on timeout, Baseline 2024). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
[^mdn-any]: MDN Web Docs — AbortSignal: any() static method (combine signals, first-reason wins, Baseline 2024). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static
[^fetch-spec]: WHATWG Fetch Standard — "Add a `timeout` option, to prevent hanging" (issue #951; fetch has no built-in timeout option). https://github.com/whatwg/fetch/issues/951
[^react-effects]: React — Synchronizing with Effects (fetch cleanup, ignore vs. abort, race conditions). https://react.dev/learn/synchronizing-with-effects
[^react-strict]: React — `<StrictMode>` (re-runs Effects an extra time in development). https://react.dev/reference/react/StrictMode
[^axios]: Axios — Cancellation (AbortController `signal`; `CancelToken` deprecated since v0.22.0). https://axios-http.com/docs/cancellation
[^tanstack]: TanStack Query — Query Cancellation (`AbortSignal` passed to the query function). https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation
[^mdn-abortcontroller]: MDN Web Docs — AbortController interface (Baseline "widely available," across browsers since March 2019). https://developer.mozilla.org/en-US/docs/Web/API/AbortController
[^chrome-fix]: mdn/browser-compat-data issue #20381 — `AbortSignal.timeout()` surfaced `AbortError` instead of `TimeoutError` in Chromium before Chrome 124. https://github.com/mdn/browser-compat-data/issues/20381
