TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

How to Cancel JavaScript fetch() Requests with AbortController

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20265 min read5 topics
How to Cancel JavaScript fetch() Requests with AbortController
Explore the details
#JavaScript#Fetch API#AbortController#Web APIs#Node.js
October 26, 20265 min read

A fetch() request can take longer than a user is willing to wait, or its result may no longer be needed after they change a search query or leave a screen. AbortController lets you stop waiting for a request and cancel supported client-side request and response-body work. This JavaScript fetch AbortController example covers a timeout, a cancel button, error handling, and cleanup—and explains what cancellation does not guarantee.

Key Takeaways

  • Pass an AbortController's signal to fetch(), then call controller.abort() to cancel.
  • Use a timer for a timeout and a user action for manual cancellation; clear the timer and remove event listeners when the request finishes.
  • Check signal.aborted to distinguish an intentional cancellation from other errors. A failed HTTP status, such as 404, does not itself reject fetch().
  • Aborting the client request does not guarantee that the server stops work it has already started.

How AbortController works with fetch()

Create an AbortController, pass its signal in the fetch() options, and call abort() on that same controller when the request should stop:

const controller = new AbortController();

try {
  const response = await fetch("/api/data", {
    signal: controller.signal,
  });

  const data = await response.json();
  console.log(data);
} catch (error) {
  if (controller.signal.aborted) {
    console.log("Request was cancelled.");
  } else {
    console.error("Request failed:", error);
  }
}

// Call this when cancellation is needed:
// controller.abort();

The signal connects the controller to the request. Calling abort() causes a pending fetch to reject; it can also stop reading a response body if the body has not finished being consumed. A controller is single-use: after it has been aborted, its signal stays aborted. Create a new controller for each request you might cancel.

In many environments, cancellation rejects with an AbortError. Checking signal.aborted is useful when you have access to the controller's signal and want to handle cancellation without treating it as an ordinary network failure. If you only have the caught error, check error.name === "AbortError" where appropriate.

Add a timeout and a cancel button

This example shows both cancellation paths. It assumes the page provides a button with id="cancel-request" and elements with id="message" and id="results". Replace /api/search?q=coffee with an endpoint in your application.

<button id="cancel-request" type="button">Cancel request</button>
<p id="message" role="status"></p>
<pre id="results"></pre>
const cancelButton = document.querySelector("#cancel-request");
const message = document.querySelector("#message");
const results = document.querySelector("#results");

async function search() {
  const controller = new AbortController();
  let timedOut = false;

  const timeoutId = setTimeout(() => {
    timedOut = true;
    controller.abort();
  }, 5000);

  const cancelRequest = () => controller.abort();
  cancelButton.addEventListener("click", cancelRequest, { once: true });
  cancelButton.disabled = false;
  message.textContent = "Loading…";
  results.textContent = "";

  try {
    const response = await fetch("/api/search?q=coffee", {
      signal: controller.signal,
    });

    if (!response.ok) {
      throw new Error(`Request failed with HTTP ${response.status}`);
    }

    const data = await response.json();
    results.textContent = JSON.stringify(data, null, 2);
    message.textContent = "Request complete.";
  } catch (error) {
    if (controller.signal.aborted) {
      message.textContent = timedOut
        ? "The request timed out."
        : "The request was cancelled.";
    } else {
      message.textContent = "Could not load results.";
      console.error(error);
    }
  } finally {
    clearTimeout(timeoutId);
    cancelButton.removeEventListener("click", cancelRequest);
    cancelButton.disabled = true;
  }
}

search();

The five-second timer calls abort() just like the button does. The timedOut flag records why it happened so the user receives a more helpful message. The finally block runs after success, cancellation, or another error, so the timer and listener do not linger. In a page with several requests, keep each request's controller and controls scoped to that request; otherwise, a button click could cancel the wrong operation.

For supported environments, AbortSignal.timeout(milliseconds) is a compact alternative for a timeout-only signal. Check compatibility for your target browsers and Node.js versions before relying on it. If you need a manual cancel button as well, the controller-and-timer approach above makes both cancellation sources explicit.

Understand what cancellation does—and does not—mean

Aborting tells the client-side fetch operation to stop. It does not guarantee that a server, database, or other downstream service stops work already underway. By the time the client cancels, the server may have received and processed the request, or may continue processing it after the connection closes.

Treat cancellation as a way to avoid waiting for an obsolete result and to stop supported client-side work—not as a rollback mechanism. If an operation must not be applied twice or must be reversible, design that behavior on the server, for example with appropriate idempotency or cancellation handling.

Also check response.ok yourself. fetch() normally resolves when it receives an HTTP response, including responses with error statuses such as 404 or 500. An aborted request and an HTTP error are different cases and should usually produce different handling.

Common Mistakes

  • Creating a controller but not passing its signal. Without signal: controller.signal, calling abort() will not cancel that fetch.
  • Reusing an aborted controller. Its signal remains aborted; create a fresh controller for the next request.
  • Treating every error as cancellation. Network failures, invalid JSON, and non-OK HTTP responses need their own handling.
  • Assuming cancellation undoes server work. The client cannot guarantee that the server stopped processing a request it already received.
  • Leaving timeout timers or listeners in place. Clear the timer and remove listeners when the request settles.
  • Assuming a timeout is automatic. fetch() has no general application-defined timeout unless you provide one, such as a timer that calls abort().

Best Practices

  • Create one controller per cancellable operation and keep it where the UI action that cancels that operation can access it.
  • Use try/catch/finally so cancellation, failures, and cleanup are handled consistently.
  • Check response.ok before parsing a response when non-2xx statuses should be treated as errors.
  • For search-as-you-type, abort the previous request when starting a new one, and ensure only the latest result updates the UI. Cancellation helps reduce stale client-side work, but it does not guarantee the server stops processing older requests.
  • Choose a timeout appropriate to the operation. A short timeout that suits autocomplete may be wrong for a large upload or a slow connection.

Conclusion

AbortController gives JavaScript code a direct way to cancel a pending fetch() request: pass its signal, then abort when a user cancels or a timeout expires. Handle cancellation separately from other failures, clean up resources, and remember that a client abort cannot promise to undo server-side work.

References