Cancellation with AbortController

The standard, composable way to cancel fetches, timers, streams, and your own async work.

Promises have no built-in cancel. The web-standard AbortController fills that gap and is now wired through Node's core APIs — fetch, fs, streams, events.once, and child_process all accept a signal.

The model is simple: a controller owns a signal; calling controller.abort(reason) flips the signal to aborted and fires its 'abort' event. Any operation watching that signal bails out and rejects with an AbortError.

const ac = new AbortController();

// Abort automatically after 2 seconds.
setTimeout(() => ac.abort(new Error('too slow')), 2000);

try {
  const res = await fetch('https://example.com', { signal: ac.signal });
  console.log(res.status);
} catch (err) {
  if (err.name === 'AbortError') console.log('request cancelled');
}

Composing signals

AbortSignal.timeout(ms) gives you a fire-and-forget timeout signal, and AbortSignal.any([...]) combines several so any of them can cancel the work — e.g. an incoming request abort OR a global deadline.

Example

Example · javascript
// A cancellable, deadline-bounded operation that also honors an
// externally supplied signal (e.g. from the HTTP request lifecycle).
const { setTimeout: sleep } = require('node:timers/promises');

async function withDeadline(ms, externalSignal, work) {
  // Cancel when EITHER our deadline fires OR the caller aborts.
  const signal = AbortSignal.any([
    AbortSignal.timeout(ms),
    ...(externalSignal ? [externalSignal] : []),
  ]);

  return work(signal);
}

// A worker that respects the signal at every await point.
async function slowJob(signal) {
  for (let step = 0; step < 10; step++) {
    signal.throwIfAborted();          // fail fast if already cancelled
    await sleep(100, undefined, { signal }); // timers/promises honors signals
    console.log('step', step);
  }
  return 'done';
}

const requestAborted = new AbortController();
// e.g. res.on('close', () => requestAborted.abort())

try {
  const result = await withDeadline(450, requestAborted.signal, slowJob);
  console.log(result);
} catch (err) {
  // Distinguish a timeout from a client disconnect via err.cause / reason.
  console.log('aborted:', err.name, err.cause?.message ?? err.message);
}

When to use it

  • A search-as-you-type feature aborts an in-flight `fetch` call with `AbortController` each time the user types a new character so stale responses are ignored.
  • A timeout wrapper creates an `AbortController`, schedules `abort()` after 5 seconds, and passes the signal to `fetch` to enforce a hard deadline.
  • A graceful-shutdown handler creates a shared `AbortController` and passes its signal to long-running streaming operations so they stop cleanly on SIGTERM.

More examples

Cancel a fetch with AbortController

Creates an `AbortController`, passes its signal to `fetch`, and aborts the request after a timeout.

Example · js
const controller = new AbortController();
const { signal } = controller;

fetch('https://api.example.com/stream', { signal })
  .then(res => res.text())
  .then(console.log)
  .catch(err => {
    if (err.name === 'AbortError') console.log('Request cancelled');
    else throw err;
  });

// Cancel after 2 seconds
setTimeout(() => controller.abort(), 2000);

Timeout any promise with AbortSignal

Uses `AbortSignal.timeout(ms)` (Node 17.3+) to create a self-aborting signal without managing a controller manually.

Example · js
// Node 17.3+ built-in helper
const signal = AbortSignal.timeout(3000); // auto-aborts after 3s

try {
  const res  = await fetch('https://slow.api/data', { signal });
  const json = await res.json();
  console.log(json);
} catch (err) {
  if (err.name === 'TimeoutError') console.error('Timed out!');
}

Propagate cancellation through async work

Passes an `AbortSignal` into a loop and calls `signal.throwIfAborted()` at each iteration to support cooperative cancellation.

Example · js
async function processItems(items, signal) {
  for (const item of items) {
    signal.throwIfAborted(); // throws AbortError if cancelled
    await processOne(item);
  }
}

const controller = new AbortController();
processItems(largeList, controller.signal).catch(console.error);

// Cancel on SIGINT
process.on('SIGINT', () => controller.abort());

Discussion

  • Be the first to comment on this lesson.