# AbortController: how to cancel a fetch in JavaScript

> How to use AbortController and AbortSignal to cancel fetch requests and other async operations, avoiding race conditions and wasted network calls.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-08-29
- URL: https://diegobetto.com/en/abortcontroller-cancel-fetch-javascript/
- Tag: javascript, async, fetch

A search box with autocomplete that hits an API on every keystroke is the textbook example, but the problem is more general than that: any `fetch` you can trigger multiple times before the previous one finishes — a filter that changes, a tab the user reopens, a component that unmounts while the request is still in flight — has the same latent bug. Responses can arrive in a different order than the one they were sent in, and if you update state with the last response _received_ instead of the last one _requested_, the user briefly sees the wrong result.

`AbortController` exists exactly for this: it gives JavaScript a standard way to tell an already-started async operation "I don't care about your result anymore, stop."

## The problem, concretely

```javascript
async function searchProducts(query) {
  const res = await fetch(`/api/search?q=${query}`);
  const data = await res.json();
  renderResults(data); // which request wins?
}

input.addEventListener("input", (e) => searchProducts(e.target.value));
```

Type "app", then right after "apple". Two fetches go out. If the request for "app" — maybe because the server happens to be slower on that specific query — responds _after_ the one for "apple", `renderResults` gets called twice and the last call wins: the screen ends up showing results for "app", even though the user typed "apple" a while ago. It's not an error that throws an exception, it's a purely visual, silent bug, hard to reproduce reliably — the kind of bug that goes ignored for months because "it happens sometimes, no one knows why."

## The fix: one controller per request

```javascript
let currentController = null;

async function searchProducts(query) {
  currentController?.abort(); // cancel the previous request, if any
  currentController = new AbortController();

  try {
    const res = await fetch(`/api/search?q=${query}`, {
      signal: currentController.signal,
    });
    const data = await res.json();
    renderResults(data);
  } catch (err) {
    if (err.name === "AbortError") return; // canceled on purpose, not a real error
    console.error("Search failed", err);
  }
}
```

Every new call aborts the previous call's controller before creating a new one. When `abort()` is called, `fetch` rejects its promise with an `AbortError` — which is why the `catch` block explicitly recognizes it and simply returns, instead of treating it as a real network error.

> `fetch()` rejects, it doesn't resolve, when canceled: a common mistake is forgetting that
> `AbortError` goes through `catch`, not a check inside the `try` block. If your code doesn't
> distinguish `AbortError` from other network errors, you end up showing the user a "Connection
> error" message every time they simply type one more character — a bug just as annoying as the one
> you were trying to fix.

## Not just `fetch`: any API that accepts a signal

`AbortSignal` isn't specific to `fetch` — it's become a standard pattern that many native web APIs accept:

```javascript
// An event listener that removes itself
document.addEventListener("click", handler, { signal: controller.signal });

// A cancelable timer without keeping the id around for clearTimeout
setTimeout(() => console.log("done"), 1000 /* not natively, needs a wrapper */);
```

`setTimeout` has no direct native signal support, but the pattern integrates easily:

```javascript
function cancelableWait(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    signal.addEventListener("abort", () => {
      clearTimeout(id);
      reject(new DOMException("Canceled", "AbortError"));
    });
  });
}
```

The same `AbortController` can then cancel multiple operations connected to the same `signal` at once — useful when a single event (the user navigates away, a component unmounts) needs to stop several things at once, without manually tracking each of them.

## Automatic cancellation with a timeout

A practical use worth knowing: `AbortSignal.timeout()` creates a signal that cancels itself after N milliseconds, useful for never leaving a fetch hanging indefinitely on a slow network or an unresponsive server:

```javascript
const res = await fetch("/api/slow-data", {
  signal: AbortSignal.timeout(5000), // cancels automatically after 5 seconds
});
```

You can also combine multiple signals with `AbortSignal.any()` — the request cancels as soon as **any one** of the provided signals fires, useful for merging "the user canceled manually" and "the timeout expired" into the same fetch without handling them separately:

```javascript
const signal = AbortSignal.any([userController.signal, AbortSignal.timeout(5000)]);
```

## The connection to debounce and throttle

If the problem you're solving is "the user types too fast and I generate too many requests," `AbortController` and [debounce](https://diegobetto.com/debounce-throttle-javascript/) aren't alternatives, they're complementary and solve two different problems: debounce reduces **how many** requests go out over time (it waits for a pause in typing before calling the API); `AbortController` guarantees that, of the requests that go out anyway, only the last one actually counts. A well-built search field, in practice, almost always uses both together.

> It's not just for network fetches: the pattern applies any time you have an async operation whose
> result can become irrelevant before it completes — including non-network cases, like an
> `IntersectionObserver` attached to a component that unmounts, or a
> `stopPropagation`/`preventDefault` call on an event that might never arrive if the element is
> removed first. If you already handle{" "}
> [event propagation](https://diegobetto.com/javascript-stoppropagation-preventdefault/) carefully,
> `AbortSignal` is the same kind of discipline applied to async code instead of DOM events.
