jsguides

AbortController

new AbortController()

The AbortController interface represents a controller object that allows you to abort one or more DOM requests as and when required. You can signal and abort asynchronous operations like fetch requests, enabling you to implement cancellation functionality for network requests, timeouts, and other asynchronous tasks. When abort() is called on an AbortController, it triggers the abort event on the associated AbortSignal, which causes any DOM operation using that signal to throw an AbortError. This API is essential for building responsive applications where users need the ability to cancel pending operations—preventing wasted bandwidth, reducing server load, and improving user experience by eliminating zombie requests that no longer matter.

Syntax

new AbortController()
// TypeScript
const controller: AbortController = new AbortController();
const signal: AbortSignal = controller.signal;

Properties

PropertyTypeDescription
signalAbortSignalReturns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation.

Methods

MethodParametersDescription
abort()reason?: anyInvokes the abort process, triggering the abort event on the associated AbortSignal. Optional reason parameter available in modern browsers.

Examples

Basic fetch with abort

const controller = new AbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/posts/1', { signal })
  .then(response => response.json())
  .then(data => console.log('Data received:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Fetch error:', error);
    }
  });
// Abort the request after 1 second
setTimeout(() => controller.abort(), 1000);
// Output (after 1 second):
// Fetch aborted

This minimal example connects a setTimeout to controller.abort(). After one second, the abort fires and the fetch promise rejects with an AbortError. The catch handler checks error.name to distinguish intentional cancellation from real network errors — always do this check, or you will silently swallow genuine failures.

Implementing a request timeout

function fetchWithTimeout(url, timeout = 3000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  
  return fetch(url, { signal: controller.signal })
    .then(response => {
      clearTimeout(id);
      return response;
    })
    .catch(error => {
      clearTimeout(id);
      if (error.name === 'AbortError') {
        throw new Error(`Request timed out after ${timeout}ms`);
      }
      throw error;
    });
}
fetchWithTimeout('https://jsonplaceholder.typicode.com/users/1', 2000)
  .then(r => r.json())
  .then(console.log)
  .catch(err => console.error(err.message));
// Output (if request takes > 2 seconds):
// Request timed out after 2000ms

Rather than hardcoding the delay, this wraps the pattern in a reusable fetchWithTimeout function. It clears the timeout on success to avoid unnecessary aborts, and on failure it distinguishes timeouts from other errors. The caller gets a clean error message instead of a bare AbortError.

Using AbortController with async/await

async function fetchUserData(userId, signal) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`,
    { signal }
  );
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  
  return response.json();
}
async function loadUserWithCancel() {
  const controller = new AbortController();
  
  const loadPromise = fetchUserData(1, controller.signal);
  
  // Cancel after 500ms
  setTimeout(() => controller.abort(), 500);
  
  try {
    const user = await loadPromise;
    console.log('User loaded:', user.name);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Operation cancelled by user');
    } else {
      console.error('Error:', error.message);
    }
  }
}
loadUserWithCancel();
// Output:
// Operation cancelled by user

The async/await version shows the same pattern with a try/catch instead of .catch(). The signal is passed explicitly to each async function that might need cancellation — this is the convention in both browser and Node.js APIs: functions that perform async work accept an optional signal parameter.

Aborting multiple concurrent requests

async function fetchMultipleUrls(urls) {
  const controller = new AbortController();
  
  const fetchPromises = urls.map(url => 
    fetch(url, { signal: controller.signal })
      .then(r => r.json())
  );
  
  // Abort all if any request takes too long
  const timeoutId = setTimeout(() => {
    console.log('Request timed out, aborting all...');
    controller.abort();
  }, 3000);
  
  try {
    const results = await Promise.allSettled(fetchPromises);
    clearTimeout(timeoutId);
    
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`URL ${urls[index]} succeeded:`, result.value);
      } else {
        console.log(`URL ${urls[index]} failed:`, result.reason.name);
      }
    });
  } catch (error) {
    console.error('All requests aborted');
  }
}
fetchMultipleUrls([
  'https://jsonplaceholder.typicode.com/users/1',
  'https://jsonplaceholder.typicode.com/users/2'
]);
// Output:
// URL https://jsonplaceholder.typicode.com/users/1 succeeded: { id: 1, name: 'Leanne Graham', ... }
// URL https://jsonplaceholder.typicode.com/users/2 succeeded: { id: 2, name: 'Ervin Howell', ... }

Sharing one signal across multiple fetch calls means a single controller.abort() cancels them all. The example uses Promise.allSettled so you can see which requests finished and which were aborted. This pattern works well for page transitions where you want to cancel every in-flight request at once.

Listening to abort events

const controller = new AbortController();
const { signal } = controller;
signal.addEventListener('abort', () => {
  console.log('Operation has been aborted');
});
signal.addEventListener('abort', () => {
  console.log('Cleanup completed');
});
// Trigger abort
controller.abort();
// Output:
// Operation has been aborted
// Cleanup completed

Multiple listeners can attach to the same abort event, which makes the signal useful as a coordination point. When the controller aborts, every listener fires, letting you clean up timers, close connections, and reset UI from separate modules that all share the same signal.

Common Patterns

Cancellation token pattern

function createCancellableOperation() {
  const controller = new AbortController();
  
  const operation = async () => {
    const response = await fetch('/api/data', { signal: controller.signal });
    return response.json();
  };
  
  return {
    execute: operation,
    cancel: () => controller.abort(),
    signal: controller.signal
  };
}
const { execute, cancel, signal } = createCancellableOperation();
// Later: cancel();

The token pattern bundles a controller with an async operation and exposes a clean { execute, cancel } interface. The caller doesn’t need to know about signals or controllers — just call cancel(). This encapsulation works well in library code where you want to hide the cancellation mechanism from consumers.

Debounced search with abort

async function search(query, signal) {
  const response = await fetch(`/api/search?q=${query}`, { signal });
  return response.json();
}
function debouncedSearch(query, delay = 300) {
  let controller;
  let timeoutId;
  
  return new Promise((resolve, reject) => {
    // Cancel previous request
    if (controller) controller.abort();
    
    controller = new AbortController();
    
    clearTimeout(timeoutId);
    timeoutId = setTimeout(async () => {
      try {
        const results = await search(query, controller.signal);
        resolve(results);
      } catch (error) {
        if (error.name !== 'AbortError') reject(error);
      }
    }, delay);
  });
}

In a typeahead search box, every keystroke could fire a new fetch. This pattern aborts the previous request before starting the next one, so only the most recent query’s results arrive. The combination of debouncing and abort keeps the UI responsive without race conditions from stale responses.

Abort reason (modern browsers)

const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
  .catch(error => {
    console.log('Abort reason:', error.reason); // "User clicked cancel"
    console.log('Error name:', error.name);      // "AbortError"
  });
controller.abort('User clicked cancel');

Modern browsers let you pass a reason to abort(). The rejection’s reason property carries that value, so your error handler can distinguish “user cancelled” from “timed out” from “navigated away.” Always include a descriptive reason string — it makes debugging much easier than staring at a bare AbortError.

AbortSignal.timeout() — a simpler timeout pattern

Node.js 17.3+ and modern browsers provide AbortSignal.timeout(ms) as a shortcut for the common controller-plus-setTimeout pattern:

// Before: manual setup
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 3000);
fetch(url, { signal: controller.signal })
  .finally(() => clearTimeout(id));

// After: built-in timeout signal
fetch(url, { signal: AbortSignal.timeout(3000) });

AbortSignal.timeout() returns a signal that fires after the given milliseconds. It cannot be manually triggered — use a full AbortController when you also need user-initiated cancellation.

Why AbortController exists

AbortController solves a practical problem in async code: sometimes a request or task is no longer useful, and continuing to wait wastes time and resources. By giving you a shared signal, the API lets several operations respond to the same cancellation event. That makes it useful for search boxes, route changes, file reads, and long-running background work where the user may move on before the operation completes.

Signal-Driven Design

The controller is only half of the story; the AbortSignal is what consumers actually watch. That split is deliberate. The controller creates and triggers cancellation, while the signal is passed into APIs that can react to it. Once you get used to that pattern, it becomes easy to design your own functions so they accept a signal argument and behave consistently with browser and Node.js APIs.

One controller can cancel many operations

A single AbortController signal can be passed to multiple async operations at once. Calling abort() once cancels all of them:

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

// All three share the same signal
const p1 = fetch('/api/users', { signal });
const p2 = fetch('/api/posts', { signal });
const p3 = fetch('/api/comments', { signal });

// Cancel all at once
controller.abort();

This is more efficient than creating separate controllers per request when you want all-or-nothing cancellation, such as when the user navigates away from a page.

abort() is idempotent

Calling abort() on an already-aborted controller does nothing — it does not throw and does not fire the abort event a second time. Once a signal is aborted, its aborted property stays true and any new operation that receives the signal is immediately rejected with an AbortError.

See Also

  • fetch() — HTTP requests with abort signal support
  • Promise — Underlying promise handling for async operations