This error occurs when an asynchronous operation in Node.js is deliberately cancelled using AbortController or times out. It indicates that the operation was terminated before completion, either programmatically or due to exceeding a time limit.
The "Cancelled" error in Node.js typically manifests as an AbortError DOMException when an operation is cancelled using the AbortController API. This is a controlled cancellation mechanism that allows developers to abort ongoing asynchronous operations like HTTP requests, file system operations, or long-running processes. When an AbortSignal is triggered (via `abort()` or `AbortSignal.timeout()`), any operation listening to that signal will immediately stop execution and reject its promise with an AbortError. This is not a bug but an intentional design pattern for managing the lifecycle of asynchronous operations. The error message "Cancelled (operation cancelled by user or timeout)" indicates that the operation was explicitly terminated rather than failing due to network issues, invalid data, or other runtime errors. This distinction is important for error handling logic.
First, determine whether this is an expected AbortError that should be handled gracefully:
try {
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
const data = await response.json();
} catch (error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
console.log('Request was cancelled or timed out');
// Handle gracefully - this is expected behavior
return;
}
// Only throw unexpected errors
throw error;
}Check the error name to distinguish cancellations from genuine failures.
If legitimate operations are being cancelled due to insufficient timeout values, adjust the duration:
// Before: Too short for slow APIs
const signal = AbortSignal.timeout(1000); // 1 second
// After: More realistic timeout
const signal = AbortSignal.timeout(10000); // 10 seconds
const response = await fetch(apiUrl, { signal });Monitor your operations to determine appropriate timeout values based on typical response times.
Remove event listeners after operations complete to prevent memory leaks:
const controller = new AbortController();
const signal = controller.signal;
const abortHandler = () => {
console.log('Operation aborted');
};
signal.addEventListener('abort', abortHandler);
try {
await someAsyncOperation(signal);
} finally {
// Always remove listener when done
signal.removeEventListener('abort', abortHandler);
}This prevents issues with long-lived AbortController instances.
Prevent unnecessary work by checking if the signal is already aborted:
async function performTask(signal) {
// Check immediately before starting
if (signal.aborted) {
throw new DOMException('Operation already cancelled', 'AbortError');
}
// Proceed with the operation
const result = await expensiveOperation();
// Check periodically during long operations
if (signal.aborted) {
throw new DOMException('Operation cancelled mid-execution', 'AbortError');
}
return result;
}This avoids starting work that will immediately be cancelled.
When cancelling POST requests with bodies, handle potential unhandled error events:
const controller = new AbortController();
try {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(data),
signal: controller.signal,
headers: { 'Content-Type': 'application/json' }
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('POST request cancelled');
return null; // or appropriate default
}
throw error;
}Always wrap cancellable POST requests in try-catch blocks to handle abort errors gracefully.
Add exponential backoff for operations that may legitimately need more time:
async function fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const timeout = 5000 * Math.pow(2, i); // Exponential: 5s, 10s, 20s
const signal = AbortSignal.timeout(timeout);
const response = await fetch(url, { signal });
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError' && i < maxRetries - 1) {
console.log(`Attempt ${i + 1} timed out, retrying...`);
lastError = error;
continue;
}
throw error;
}
}
throw lastError;
}This handles transient network issues that cause timeouts.
AbortController with Multiple Operations
You can use a single AbortController to cancel multiple related operations simultaneously:
const controller = new AbortController();
const signal = controller.signal;
const promises = [
fetch('/api/users', { signal }),
fetch('/api/posts', { signal }),
fetch('/api/comments', { signal })
];
// Cancel all three requests at once
setTimeout(() => controller.abort(), 3000);
try {
const results = await Promise.all(promises);
} catch (error) {
// All three will fail with AbortError
}Node.js Core API Support
The AbortController API is supported across many Node.js core modules: fs, net, http, events, child_process, readline, and stream. You can use the same cancellation pattern consistently:
const { readFile } = require('fs/promises');
const controller = new AbortController();
setTimeout(() => controller.abort(), 100);
try {
const data = await readFile('/large-file.txt', {
signal: controller.signal
});
} catch (error) {
if (error.name === 'AbortError') {
console.log('File read cancelled');
}
}Custom Cancellable Operations
You can make your own functions respect AbortSignals:
async function customOperation(signal) {
return new Promise((resolve, reject) => {
if (signal.aborted) {
return reject(new DOMException('Aborted', 'AbortError'));
}
const abortHandler = () => {
cleanup();
reject(new DOMException('Aborted', 'AbortError'));
};
signal.addEventListener('abort', abortHandler);
// Your async work here...
function cleanup() {
signal.removeEventListener('abort', abortHandler);
}
});
}Distinguishing User Aborts from Timeouts
Node.js 17.3.0+ allows differentiation:
catch (error) {
if (error.name === 'TimeoutError') {
// Automatic timeout
} else if (error.name === 'AbortError') {
// Manual abort() call
}
}This helps provide more specific user feedback about why an operation was cancelled.
Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Listener already called (once event already fired)
EventEmitter listener already called with once()
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files