Quick answer
AbortError means an AbortController cancelled the operation — a manual cancel, a timeout, or a framework cleanup. It is usually intentional, not a network failure. If you aborted on purpose, catch and ignore it with if (err.name === 'AbortError') return;. Only investigate when nothing in your code meant to abort.
The exact error string
const controller = new AbortController();
const promise = fetch('/api/data', { signal: controller.signal });
controller.abort();
await promise;
// DOMException: The operation was aborted. (Chrome)
// AbortError: The operation was aborted. (.name === 'AbortError')
// DOMException: The user aborted a request. (Firefox wording)
// AbortError: signal is aborted without reason (no reason passed)
The key insight: this is almost always the success signal of a cancel, not a failure. The whole point of AbortController is to stop an in-flight request, and AbortError is how that stop is reported. The real question is never "how do I stop the error" but "did I mean to abort?"
Did you abort on purpose? (the decision that matters)
Split every AbortError into one of two buckets:
- Intentional — a timeout fired, the user navigated away, a newer search request superseded an older one, or a React effect cleaned up. Here
AbortErroris expected; swallow it. - Unexpected — nothing in your code meant to cancel. Here it points to a bug: a shared controller reused across requests, a signal aborted too early, or a timeout set unrealistically short.
Fix 1: catch and ignore an intentional abort
Branch on err.name. A deliberate cancel should never surface as an error in the console or UI:
try {
const res = await fetch(url, { signal });
const data = await res.json();
render(data);
} catch (err) {
if (err.name === 'AbortError') return; // expected — we cancelled
// a real failure: network down, parse error, etc.
showError(err);
}
Fix 2: the React useEffect cleanup pattern
The highest-volume modern cause is React. In React 18 StrictMode, development mounts each component, unmounts it, and remounts it — running the effect's cleanup in between. If your effect correctly aborts the fetch on cleanup, that first request is cancelled and throws AbortError. This is the recommended pattern, not a bug — you just need to ignore the abort:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${id}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser)
.catch(err => {
if (err.name === 'AbortError') return; // StrictMode cleanup — ignore
setError(err);
});
return () => controller.abort(); // cleanup cancels in-flight fetch
}, [id]);
Aborting on cleanup also fixes the classic "can't perform a React state update on an unmounted component" warning and prevents a stale response from overwriting a newer one when id changes quickly.
Fix 3: a fetch timeout with AbortController
Cancelling after a deadline is the other big intentional case. The modern one-liner is AbortSignal.timeout(); the portable version uses a manual controller:
// Modern (Node 17.3+, current browsers): rejects with TimeoutError after 5s
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
// Portable: manual controller + setTimeout
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} finally {
clearTimeout(t); // always clear so a fast response isn't aborted
}
Forgetting clearTimeout is a subtle bug: the timer can fire after a successful response and abort a follow-up request that reused the same controller.
Fix 4: give abort a reason ("aborted without reason")
If you see signal is aborted without reason, you called abort() with no argument. Pass a reason so the rejection carries a cause you can branch on:
controller.abort(new DOMException('Request timed out', 'TimeoutError'));
// or a simple value:
controller.abort('user navigated away');
// then distinguish causes:
catch (err) {
if (signal.reason === 'user navigated away') return;
if (err.name === 'TimeoutError') showTimeout();
}
Aborting in axios (CanceledError)
Axios uses the same AbortController, but reports the cancel as its own CanceledError (older versions: axios.isCancel / Cancel). Handle it the same way — treat a deliberate cancel as a no-op:
import axios, { isCancel } from 'axios';
const controller = new AbortController();
axios.get('/api/data', { signal: controller.signal })
.then(res => render(res.data))
.catch(err => {
if (isCancel(err)) return; // expected cancel — ignore
if (err.name === 'CanceledError') return;
showError(err);
});
controller.abort(); // cancels the axios request
Cancelling many requests at once
One controller can abort any number of requests that share its signal — useful for tearing down a whole screen's worth of fetches. And AbortSignal.any() combines several signals so a request is cancelled if any of them fires (for example, a per-request timeout or a global "user navigated away"):
// One controller cancels a batch:
const controller = new AbortController();
const { signal } = controller;
const results = await Promise.all([
fetch('/a', { signal }),
fetch('/b', { signal }),
fetch('/c', { signal }),
]).catch(err => {
if (err.name === 'AbortError') return []; // whole batch cancelled
throw err;
});
// controller.abort() now stops all three.
// Combine a timeout with a manual cancel:
const userCancel = new AbortController();
const combined = AbortSignal.any([
userCancel.signal,
AbortSignal.timeout(5000),
]);
fetch('/slow', { signal: combined }); // aborts on whichever fires first
Remember a controller cannot be reused after it aborts — for a fresh batch, create a new AbortController.
When it really is a bug
If you never call abort() and still get AbortError, check for: a single AbortController shared across multiple requests (aborting one aborts all and the controller cannot be reused after abort() — create a fresh one per request); a signal that was already aborted before the fetch started; or a timeout so short that normal responses never arrive. Each produces an abort you did not intend.
Debugging checklist
- ✓ Ask first: did I mean to abort (cancel / timeout / cleanup)? If yes, ignore it
- ✓ Swallow intentional aborts with
if (err.name === 'AbortError') return; - ✓ React? Abort in the
useEffectcleanup and ignore the StrictMode abort - ✓ Timeout? Use
AbortSignal.timeout(ms), andclearTimeouton the manual version - ✓ Create a new
AbortControllerper request — they can't be reused afterabort() - ✓ "aborted without reason"? Pass a reason to
abort()
Frequently Asked Questions
What does 'AbortError: The operation was aborted' mean?
It means a fetch (or other abortable operation) was cancelled because an AbortController called abort(), or its AbortSignal fired. The request did not fail on the network — it was deliberately stopped, either by your own code, a timeout, or a framework cleanup.
Is AbortError a real error or expected?
Most of the time it is expected. If you aborted on purpose — a timeout, a user navigating away, a newer request superseding an old one — AbortError is the normal signal that the cancel worked, and you should catch and ignore it. It is only a bug when nothing in your code intended to abort.
How do I ignore an intentional AbortError?
Check the error name in your catch block: if (err.name === 'AbortError') return; — swallow it and do nothing. Only report or retry for other errors. This stops a deliberate cancel from showing up as an error in the console or UI.
Why do I get AbortError in React useEffect?
React 18 StrictMode mounts, unmounts, and remounts components in development, running each effect's cleanup. If your effect aborts the fetch on cleanup (the recommended pattern), the first run's request is aborted and throws AbortError. This is correct behaviour — catch and ignore AbortError so the false alarm disappears.
How do I add a fetch timeout with AbortController?
Use AbortSignal.timeout(ms) as the fetch signal, or create an AbortController and call abort() from a setTimeout. When the time elapses the fetch rejects with AbortError (or a TimeoutError for AbortSignal.timeout in newer runtimes), which you handle as a timeout case.
What is 'signal is aborted without reason'?
It is the same family of error — the AbortSignal was aborted but no reason argument was passed to abort(). Pass a reason, e.g. controller.abort(new DOMException('timeout', 'TimeoutError')) or controller.abort('user cancelled'), so the rejection carries a meaningful cause you can branch on.
Building and testing API calls?
See practical patterns for fetch, timeouts, and inspecting responses in our API testing guide.