DEV Community

Cover image for Why JavaScript AbortController Is Not Canceling fetch() Requests (And How to Fix It)
Emily Scott
Emily Scott

Posted on

Why JavaScript AbortController Is Not Canceling fetch() Requests (And How to Fix It)

Why JavaScript AbortController Is Not Canceling fetch() Requests (And How to Fix It)
A less common—but very real—problem many developers face is this:

You try to cancel a fetch() request, but the request still continues.

Or worse, the UI updates with old data after the user has already moved on.

This happens a lot in:

  • Search suggestions
  • Live filtering
  • React components
  • Route changes
  • Infinite scroll
  • File uploads
  • Dashboard refreshes

It creates strange bugs that are difficult to trace, especially in modern frontend applications.

Let’s fix it properly.


The Problem

Suppose you are building a live search feature.

You write this:

const controller = new AbortController();

fetch("/api/search?q=javascript", {
  signal: controller.signal
})
  .then((res) => res.json())
  .then((data) => {
    console.log(data);
  });

controller.abort();
Enter fullscreen mode Exit fullscreen mode

You expect the request to stop immediately.

Instead, sometimes:

  • The request still appears in the Network tab
  • Old results still render
  • Unexpected errors appear

Very confusing.

It feels like AbortController is broken.

But usually, the real problem is implementation.


Why This Happens

AbortController does not magically stop everything.

It only tells fetch():

Cancel this request if possible

When the request is aborted:

  • The Promise rejects
  • An error is thrown
  • Your .then() chain may still behave unexpectedly if not handled correctly

Many developers forget this part.

That causes bugs.


Common Mistake #1: Forgetting to Handle the Abort Error

Developers often write this:

fetch(url, {
  signal: controller.signal
})
  .then((res) => res.json())
  .then((data) => {
    setResults(data);
  });
Enter fullscreen mode Exit fullscreen mode

But they forget .catch().

When the request is aborted, JavaScript throws an error.

Without proper handling, debugging becomes messy.


Step 1: Always Handle Abort Errors

Use this instead:

fetch(url, {
  signal: controller.signal
})
  .then((res) => res.json())
  .then((data) => {
    setResults(data);
  })
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Request was canceled");
    } else {
      console.error(error);
    }
  });
Enter fullscreen mode Exit fullscreen mode

This is the first and most important fix.


Common Mistake #2: Reusing the Same Controller

Wrong approach:

const controller = new AbortController();

function search() {
  fetch(url, {
    signal: controller.signal
  });
}
Enter fullscreen mode Exit fullscreen mode

The problem is simple:

Once aborted, that controller stays aborted.

You cannot safely reuse it.

Many developers miss this.


Step 2: Create a New Controller for Every Request

Correct approach:

function search() {
  const controller = new AbortController();

  fetch(url, {
    signal: controller.signal
  });

  return controller;
}
Enter fullscreen mode Exit fullscreen mode

Fresh controller.

Fresh request.

Much safer.


Real React Example

This problem happens constantly in React.

A user types quickly in a search box.

Older API requests finish later and overwrite newer results.

That creates bad UX.

Very common.


Step 3: Cancel Previous Requests in React

Use this pattern:

let controller;

function handleSearch(query) {
  if (controller) {
    controller.abort();
  }

  controller = new AbortController();

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then((res) => res.json())
    .then((data) => {
      setResults(data);
    })
    .catch((error) => {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    });
}
Enter fullscreen mode Exit fullscreen mode

Now only the latest request matters.

Much cleaner.

Much safer.

Better UX.


Another Hidden Problem

Some developers think this:

controller.abort();
Enter fullscreen mode Exit fullscreen mode

cancels JavaScript itself.

It does not.

It only affects the network request.

If your .then() has already started running, you must still control that logic manually.

This is a very important detail.


Step 4: Protect UI Updates

Safer example:

fetch(url, {
  signal: controller.signal
})
  .then((res) => res.json())
  .then((data) => {
    if (!controller.signal.aborted) {
      setResults(data);
    }
  });
Enter fullscreen mode Exit fullscreen mode

This helps prevent stale UI updates.

Very useful in real applications.


Quick Debug Rule

Whenever old API data appears, ask yourself:

Did I cancel the request, or did I only assume it was canceled?

That question usually reveals the real issue immediately.


Final Thoughts

AbortController is powerful—but only when used correctly.

Remember:

  • Always handle AbortError
  • Never reuse the same controller
  • Create a new controller for each request
  • Protect UI updates after aborting
  • Especially important in React search and filtering

This is a less common bug, but when it happens, it becomes a nightmare.

Fixing it properly saves serious debugging time.


Your Turn

Have you ever had old API results overwrite new ones in your UI?

That is usually where the AbortController lesson begins.

Thanks for reading,
Emily Idioms

Top comments (0)