DEV Community

Cover image for The complete guide to the AbortController API
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

1

The complete guide to the AbortController API

Written by Joseph Mawa✏️

This tutorial will offer a complete guide on how to use the AbortController and AbortSignal APIs in both your backend and frontend. In our case, we’ll focus on Node.js and React.

Introduction to the AbortController API

The AbortController API became part of Node in v15.0.0. It is a handy API for aborting some asynchronous processes, similar to the AbortController interface in the browser environment.

You need to create an instance of the AbortController class to use it:

const controller = new AbortController();
Enter fullscreen mode Exit fullscreen mode

An instance of the AbortController class exposes the abort method and the signal property.

Invoking the abort method emits the abort event to notify the abortable API watching the controller about the cancellation. You can pass an optional reason for aborting to the abort method. If you don’t include a reason for the cancellation, it defaults to the AbortError.

To listen for the abort event, you need to add an event listener to the controller’s signal property using the addEventListener method so that you run some code in response to the abort event. An equivalent method for removing the event listener is the removeEventListener method.

The code below shows how to add and remove the abort event listener with the addEventListener and removeEventListener methods:

const controller = new AbortController();
const { signal } = controller;

const abortEventListener = (event) => {
  console.log(signal.aborted); // true
  console.log(signal.reason); // Hello World
};

signal.addEventListener("abort", abortEventListener);
controller.abort("Hello World");
signal.removeEventListener("abort", abortEventListener);
Enter fullscreen mode Exit fullscreen mode

The controller’s signal has a reason property, which is the reason you pass to the abort method at cancellation. Its initial value is undefined. The value of the reason property changes to the reason you pass as an argument to the abort method or defaults to AbortError if you abort without providing a reason for the cancellation. Similarly, the signal’s aborted property with an initial value of false changes to true after aborting.

Unlike in the above example, practical use of the AbortController API involves passing the signal property to any cancelable asynchronous API. You can pass the same signal property to as many cancelable APIs. The APIs will then wait for the controller’s “signal” to abort the asynchronous operation when you invoke the abort method.

Most of the built-in cancellation-aware APIs implement the cancellation out of the box for you. You pass in the controller’s signal property to the API, and it aborts the process when you invoke the controller’s abort method.

However, to implement a custom cancelable promise-based functionality, you need to add an event listener which listens for the abort event and cancels the process from the event handler when the event is triggered.


Editor’s note: This article was updated by Chizaram Ken in March 2025 to include more comprehensive information on frontend and backend use cases for AbortController in both Node.js and React.


Why use AbortController?

JavaScript is a single-threaded programming language. Depending on the runtime environment, the JavaScript engine offloads asynchronous processes, such as making network requests, file system access, and other time-consuming jobs, to some APIs to achieve asynchrony.

Ordinarily, we expect the result of an asynchronous operation to succeed or fail. However, the process can also take more time than anticipated, or you may no longer need the results when you receive them.

Therefore, it is logical to terminate an asynchronous operation that has taken more time than it should or whose result you don’t need. However, doing so natively was a daunting challenge for a very long time.

AbortController was introduced in Node v15.0.0 to abort certain asynchronous operations natively in Node.

How to use AbortController in Node.js

The AbortController API is a relatively new addition to Node. Therefore, a few asynchronous APIs support it at the moment. These APIs include the new Fetch API, timers, fs.readFile, fs.writeFile, http.request, and https.request.

We will learn how to use the AbortController API with some of the mentioned APIs. Because the APIs work with AbortController in a similar way, we’ll only look at the Fetch and fs.readFile API.

How to use AbortController with the Fetch API

Historically, node-fetch has been the de facto HTTP client for Node. With the introduction of the Fetch API in Node.js, however, that is about to change. Fetch is one of the native APIs whose behavior you can control with the AbortController API.

As explained above, you pass the signal property of the AbortController instance to any abortable, promise-based API like Fetch. The example below illustrates how you can use it with the AbortController API:

const url = "https://jsonplaceholder.typicode.com/todos/1";

const controller = new AbortController();
const signal = controller.signal;

const fetchTodo = async () => {
  try {
    const response = await fetch(url, { signal });
    const todo = await response.json();
    console.log(todo);
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Operation timed out");
    } else {
      console.error(err);
    }
  }
};

fetchTodo();

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

The trivial example above illustrates how to use the AbortController API with the Fetch API in Node. However, in a real-world project, you don’t start an asynchronous operation and abort it immediately like in the code above.

It is also worth emphasizing that fetch is still an experimental feature in Node. Its features might change in future versions.

How to use AbortController with fs.readFile

In the previous section, we looked at using AbortController with the Fetch API. Similarly, you can use this API with the other cancelable APIs.

You can do this by passing the controller’s signal property to the API’s respective function. The code below shows how to use AbortController with fs.readFile:

const fs = require("node:fs");

const controller = new AbortController();
const { signal } = controller;

fs.readFile("data.txt", { signal, encoding: "utf8" }, (error, data) => {
  if (error) {
    if (error.name === "AbortError") {
      console.log("Read file process aborted");
    } else {
      console.error(error);
    }
    return;
  }
  console.log(data);
});

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

Since the other cancelable APIs work similarly with AbortController, we won’t cover them here.

Introduction to AbortSignal

Each AbortController class instance has a corresponding AbortSignal class instance, accessible using the signal property. However, AbortSignal has functions such as the AbortSignal.timeout static method that you can also use independent of AbortController.

The AbortSignal class extends the EventTarget class and can receive the abort event. Therefore, you can use the addEventListener and removeEventListener methods to add and remove listeners for the abort event:

const controller = new AbortController();
const { signal } = controller;

signal.addEventListener(
  "abort",
  () => {
    console.log("First event handler");
  },
  { once: true }
);
signal.addEventListener(
  "abort",
  () => {
    console.log("Second event handler");
  },
  { once: true }
);

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

As in the above example, you can add as many event handlers as possible. Invoking the controller’s abort method will trigger all the event listeners. Removing the abort event listener after aborting the asynchronous process is standard practice to prevent memory leaks.

You can pass the optional third argument { once: true } to addEventListener as we did above instead of using removeEventListener to remove the event listener. The optional third argument will ensure Node triggers the event listener once and removes it.

How to use AbortSignal to time out async operations

As mentioned above, in addition to using it with AbortController, the AbortSignal class has some handy methods you might need. One of these methods is the AbortSignal.timeout static method. As its name suggests, you can use it to abort cancelable asynchronous processes on timeout.

It takes the number of milliseconds as an argument and returns a signal you can use to timeout an abortable operation. The code below shows how you can implement it with the Fetch API:

const signal = AbortSignal.timeout(200);
const url = "https://jsonplaceholder.typicode.com/todos/1";

const fetchTodo = async () => {
  try {
    const response = await fetch(url, { signal });
    const todo = await response.json();
    console.log(todo);
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("Operation timed out");
    } else {
      console.error(err);
    }
  }
};

fetchTodo();
Enter fullscreen mode Exit fullscreen mode

You can use AbortSignal.timeout similarly with the other abortable APIs.

How to use AbortSignal to cancel multiple operations

When you are working with more than one abort signal, you can combine them using the AbortSignal.any(). This is really helpful when you need multiple ways to cancel the same operation.

In the example below, I will create two controllers,. The first one will be controlled by the user of the API, and the other will be used for internal timeout purposes. If either one aborts, the event listener gets removed:

// Create two separate controllers for different concerns
const userController = new AbortController();
const timeoutController = new AbortController();

// Set up a timeout that will abort after 5 seconds
setTimeout(() => timeoutController.abort(), 5000);

// Register an event listener that can be aborted by either signal
document.addEventListener('click', handleUserClick, {
  signal: AbortSignal.any([userController.signal, timeoutController.signal])
});
Enter fullscreen mode Exit fullscreen mode

If any signal in the group aborts, the combined signal immediately aborts, ignoring any subsequent abort events. This provides you with a clean separation of concerns.

Working with streams and AbortSignals

AbortSignals allows you to easily stop a stream. For instance, if your intentions are to stop a stream because you got the value you are looking for, or you want to do something else entirely, you can just use AbortSignal this way:

const abortController = new AbortController();
const { signal } = abortController;

const uploadStream = new WritableStream({
  /* implementation */
}, { signal });

// To abort:
abortController.abort();
Enter fullscreen mode Exit fullscreen mode

In the example above, the AbortController creates a signal that, when passed to a WritableStream constructor options, allows you to cancel the stream's processing by calling abort() on the controller.

How to implement an abortable API using AbortController and AbortSignal

As highlighted in the previous section, several built-in asynchronous APIs support the AbortController API. However, you can also implement a custom abortable promise-based API that uses AbortController.

Like the built-in APIs, your API should take the signal property of an AbortController class instance as an argument as in the example below. It is standard practice for all APIs capable of using the AbortController API:

const myAbortableApi = (options = {}) => {
  const { signal } = options;

  if (signal?.aborted === true) {
    throw new Error(signal.reason);
  }

  const abortEventListener = () => {
    // Abort API from here
  };
  if (signal) {
    signal.addEventListener("abort", abortEventListener, { once: true });
  }
  try {
    // Run some asynchronous code
    if (signal?.aborted === true) {
      throw new Error(signal.reason);
    }
    // Run more asynchronous code
  } finally {
    if (signal) {
      signal.removeEventListener("abort", abortEventListener);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

In the example above, we first checked whether the value of signal’s aborted property is true. If so, it means the controller’s abort method has been invoked. Therefore, we throw an error.

Like mentioned in the previous sections, you can register the abort event listener using the addEventListener method. To prevent memory leaks, we are passing the { once: true } option as the third argument to the addEventListener method. It removes the event handler after handling the abort event.

Similarly, we removed the event listener using the removeEventListener in the finally block to prevent memory leaks. If you don’t remove it, and the myAbortableApi function runs successfully without aborting, the event listener you added will still be attached to the signal even after exiting the function.

How to use the Abort Controller in React

The AbortController API is particularly useful to React developers, but in different ways.

When you want to use event listeners, you will need to add an addEventListener, and then carefully remove each one with removeEventListener in a cleanup function.

Although this works, it's a bit tiring and prone to typographic error. Let us look into a real example that might look familiar.

For instance, you are building a dashboard that tracks mouse movements, listens for keyboard shortcuts, monitors scroll position, and responds to window resizing. Those are four different event listeners to manage.

In a normal scenario, you’d do this:

useEffect(() => {
  // Define all your handler functions
  const handleMouseMove = (e) => { /* update state */ };
  const handleKeyPress = (e) => { /* update state */ };
  const handleScroll = () => { /* update state */ };
  const handleResize = () => { /* update state */ };

  // Add all the listeners
  document.addEventListener('mousemove', handleMouseMove);
  document.addEventListener('keydown', handleKeyPress);
  window.addEventListener('scroll', handleScroll);
  window.addEventListener('resize', handleResize);

  // Return a cleanup function that removes them all
  return () => {
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('keydown', handleKeyPress);
    window.removeEventListener('scroll', handleScroll);
    window.removeEventListener('resize', handleResize);
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

This works, but you will need to keep those function references around just so you can pass the same reference to both functions. Using AbortController, it looks like this:

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

  // Define all your handler functions
  const handleMouseMove = (e) => { /* update state */ };
  const handleKeyPress = (e) => { /* update state */ };
  const handleScroll = () => { /* update state */ };
  const handleResize = () => { /* update state */ };

  // Add all the listeners with the signal
  document.addEventListener('mousemove', handleMouseMove, { signal });
  document.addEventListener('keydown', handleKeyPress, { signal });
  window.addEventListener('scroll', handleScroll, { signal });
  window.addEventListener('resize', handleResize, { signal });

  // Just one line for cleanup!
  return () => controller.abort();
}, []);
Enter fullscreen mode Exit fullscreen mode

You can clean everything up with just one line, no matter the number of event listeners available. This is cleaner and less prone to error.

Most examples online you may find using AbortController in React are mostly implemented within a useEffect(). However, you don’t necessarily need a useEffect() to use an AbortController in React.

You can constantly use the AbortController for fetch requests in React, as well.

Consider, for example, a project where I will need a search feature that would fire off API requests as the user types.

The problem will be that if the user types quickly, I'll end up with multiple requests in flight. Sometimes, older requests will finish after newer ones, causing the results to jump around.

Using AbortController in React can help solve this problem:

// Key implementation of AbortController for API requests in React
import { useRef, useState } from 'react';

// Component with search functionality
const SearchComponent = () => {
  const controllerRef = useRef<AbortController>();
  const [query, setQuery] = useState<string>();
  const [results, setResults] = useState<Array<any> | undefined>();

  async function handleOnChange(e: React.SyntheticEvent) {
    const target = e.target as typeof e.target & {
      value: string;
    };

    // Update the query state
    setQuery(target.value);
    setResults(undefined);

    // Cancel any previous in-flight request
    if (controllerRef.current) {
      controllerRef.current.abort();
    }

    // Create a new controller for this request
    controllerRef.current = new AbortController();
    const signal = controllerRef.current.signal;

    try {
      const response = await fetch('/api/search', {
        method: 'POST',
        body: JSON.stringify({
          query: target.value
        }),
        signal
      });

      const data = await response.json();
      setResults(data.results);
    } catch(e) {
      // Silently catch aborted requests
      // For production, you might want to check if error is an AbortError
    }
  }

  return (
    <div>
      <input type="text" onChange={handleOnChange} />
      {/* Results rendering */}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In the example above, we created a search function that cancels previous API requests when a user types something new. Using useRef, we're able to reference and track the current request and abort it each time the input changes.

This pattern will save you countless headaches. With your fingers crossed and a strong belief in AbortController, you shouldn't get outdated results showing up after newer ones.

Conclusion

Ordinarily, an asynchronous process may succeed, fail, or take longer than anticipated. Therefore, it is logical to cancel an asynchronous operation that has taken more time than it should or whose results you don’t need. The AbortController API is a handy functionality for doing just that.

The AbortController API is globally available; you don’t need to import it. An instance of the AbortController class exposes the abort method and the signal property. The signal property is an instance of the AbortSignal class. Each AbortController class instance has a corresponding AbortSignal class instance, which you can access using the controller’s signal property.

You pass the signal property to a cancelable asynchronous API and invoke the controller’s abort method to trigger the abort process. If the built-in APIs do not meet your use case, you can also implement a custom abortable API using AbortController and AbortSignal. However, follow the best practices hinted above to prevent memory leaks.

I’ll leave you with this; the beauty of the AbortController API is that you can make virtually any asynchronous operation abortable, even those that don't natively support cancellation. Did I miss anything? Leave a comment in the comments section below.


200’s only ✔️ Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.

LogRocket Network Request Monitoring

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Heroku

Deploy with ease. Manage efficiently. Scale faster.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)