Node.js 15: Introduction to AbortController

Felipe Hlibco

If you’ve done any work with fetch in the browser, you’ve probably used AbortController. It’s the standard Web API for cancelling asynchronous operations—pass an AbortSignal to a fetch call, call abort() when you want to cancel, and the browser throws an AbortError. Clean, composable, and it works.

Node.js hasn’t had this. And honestly? The absence of a built-in cancellation primitive has been one of those quiet pain points that every backend developer just… works around. Some use timeouts. Some use custom event emitters. Some reach for p-cancelable or similar libraries. None of it is standardized—every team invents their own slightly different pattern.

That’s about to change. PR #33527 landed the AbortController implementation in Node core—it’s available behind --experimental-abortcontroller in Node 14.7.0+ nightly builds. Node 15 (due next month) will ship it without the flag.

Why Cancellation Matters Server-Side #

In the browser, the use case is pretty obvious—user navigates away, component unmounts, you abort the in-flight fetch. Server-side, the motivations are different but equally real:

Request timeouts. When an upstream service is slow, you want to cancel the call after N milliseconds rather than holding the connection open forever. Right now, most people do this with setTimeout and manual cleanup. It works, but it’s ad-hoc—and ad-hoc solutions have a way of breaking at 2 AM.

Resource cleanup. Long-running file operations, database queries, stream processing—they all need a way to signal “stop what you’re doing.” Without a standard mechanism, every library invents its own cancellation pattern. I’ve seen at least four different approaches in production codebases, and none of them compose well.

Graceful shutdown. When your process receives SIGTERM, you need to cancel in-progress work. A shared signal that propagates through your async call chain is exactly what AbortController provides—no more passing callback functions around like it’s 2010.

The API #

The API is intentionally small. Two classes, one event:

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

// Listen for abort
signal.addEventListener('abort', () => {
  console.log('Operation cancelled');
});

// Somewhere later...
ac.abort();

AbortController creates the controller. AbortSignal is the readonly token you pass to async operations. When abort() is called, the signal emits the abort event and signal.aborted becomes true.

The pattern is always the same: create a controller, pass signal to whatever you want to be cancellable, call abort() when you’re done.

Integration with Core APIs #

What makes this genuinely useful—rather than just another event pattern—is integration with Node’s core modules. The plan is AbortSignal support across fs, http, timers, child_process, and stream.

Here’s setTimeout (available in the experimental implementation):

const ac = new AbortController();

const timeout = setTimeout(() => {
  console.log('This might not run');
}, 5000, { signal: ac.signal });

// Cancel the timer
ac.abort();
// The timeout callback never fires

And with fs.readFile:

const ac = new AbortController();

fs.readFile('/large/file.dat', { signal: ac.signal }, (err, data) => {
  if (err) {
    if (err.name === 'AbortError') {
      console.log('Read was cancelled');
      return;
    }
    throw err;
  }
  // process data
});

// Cancel if it takes too long
setTimeout(() => ac.abort(), 1000);

The AbortError handling pattern matters. When an operation is cancelled via signal, it throws (or calls back with) an AbortError. Your error handling needs to distinguish between “something went wrong” and “I told it to stop.” Checking err.name === 'AbortError' is the convention—not elegant, but consistent.

Before Native Support: Polyfills #

If you’re on Node 14 LTS and can’t use the experimental flag, the node-abort-controller package on npm provides a compatible implementation:

const { AbortController } = require('node-abort-controller');

const ac = new AbortController();
// Same API, same behavior

It won’t integrate with core modules (that requires the native implementation), but it gives you the controller/signal pattern for your own code and for libraries that accept AbortSignal.

I’ve been using node-abort-controller in a few services already—mostly for request-scoped cancellation in our API layer. When a client disconnects, we abort any in-progress upstream calls. The pattern works well:

app.get('/api/data', async (req, res) => {
  const ac = new AbortController();

  // Abort if client disconnects
  req.on('close', () => ac.abort());

  try {
    const result = await fetchUpstream('/slow-service', {
      signal: ac.signal
    });
    res.json(result);
  } catch (err) {
    if (err.name === 'AbortError') {
      // Client left, nothing to send
      return;
    }
    res.status(500).json({ error: err.message });
  }
});

Composing Signals #

One thing that’s not immediately obvious from the basic API: you can compose multiple abort conditions. Want to cancel if either a timeout expires or the user disconnects? You can do that.

function withTimeout(signal, ms) {
  const ac = new AbortController();

  const timer = setTimeout(() => ac.abort(), ms);

  if (signal) {
    signal.addEventListener('abort', () => {
      clearTimeout(timer);
      ac.abort();
    });
  }

  return ac.signal;
}

// Usage: cancel after 5s OR if parent signal aborts
const combinedSignal = withTimeout(parentSignal, 5000);
await doSomething({ signal: combinedSignal });

This composability is what makes AbortController more powerful than ad-hoc timeout patterns. Each layer of your application can add its own cancellation conditions without knowing about the others—no coordination required.

What to Do Now #

If you’re on Node 14, try the experimental flag (--experimental-abortcontroller) and see how it fits your codebase. If you can’t use the flag, node-abort-controller gives you the same API surface for your application code.

The important thing is to start thinking in terms of signals rather than timeouts. When Node 15 ships and core APIs accept AbortSignal natively, codebases that already use the pattern will benefit immediately. Those that have bespoke cancellation mechanisms in every service? They’ll have a longer migration ahead of them.

I think AbortController is one of those features that seems minor in isolation but changes how you architect async workflows. Cancellation should be a first-class concern—not an afterthought bolted on with clearTimeout.