This error occurs when an asynchronous operation takes longer than the specified timeout duration to complete. The promise remains in a pending state past its deadline, triggering a timeout rejection to prevent indefinite waiting.
Promises in JavaScript/Node.js don't have built-in timeout mechanisms. When you implement a timeout wrapper around a promise (using Promise.race, AbortController, or timeout libraries), and the underlying operation doesn't resolve or reject within the specified time limit, a TimeoutError is thrown. This prevents your application from hanging indefinitely while waiting for slow network requests, database queries, or other long-running operations. The key insight is that the timeout doesn't cancel the underlying operation—it simply stops waiting for it and rejects the promise with a timeout error.
Create a timeout wrapper using Promise.race() to limit how long you wait for an operation:
function timeout(ms, promise) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`TimeoutError: Operation timeout after ${ms}ms`));
}, ms);
promise
.then((value) => {
clearTimeout(timer);
resolve(value);
})
.catch((err) => {
clearTimeout(timer);
reject(err);
});
});
}
// Usage
try {
const result = await timeout(5000, fetch('https://api.example.com/data'));
console.log('Success:', result);
} catch (err) {
if (err.message.includes('TimeoutError')) {
console.error('Request timed out');
} else {
console.error('Other error:', err);
}
}Always clear the timeout when the promise settles to prevent memory leaks.
Modern Node.js (v16.14+) supports AbortSignal.timeout() for cleaner timeout handling:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.example.com/data', {
signal: controller.signal,
});
clearTimeout(timeoutId);
const data = await response.json();
console.log('Success:', data);
} catch (err) {
if (err.name === 'AbortError') {
console.error('Request was aborted due to timeout');
} else {
console.error('Fetch failed:', err);
}
}Or use the static timeout method:
try {
const response = await fetch('https://api.example.com/data', {
signal: AbortSignal.timeout(5000),
});
const data = await response.json();
} catch (err) {
if (err.name === 'TimeoutError') {
console.error('Request timed out after 5 seconds');
}
}If timeout errors occur for operations that need more time, adjust the timeout value:
// Before: Too aggressive
await timeout(1000, heavyDatabaseQuery());
// After: More realistic for the operation
await timeout(10000, heavyDatabaseQuery());For database queries, configure connection timeouts appropriately:
const pool = new Pool({
connectionTimeoutMillis: 10000, // 10 seconds to establish connection
query_timeout: 30000, // 30 seconds for query execution
statement_timeout: 25000, // 25 seconds for statement execution
});Balance timeout values based on expected operation duration and user experience requirements.
Implement automatic retries for timeout errors that might succeed on subsequent attempts:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (err) {
lastError = err;
if (err.name === 'AbortError' && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // Exponential backoff
console.log(`Retry ${i + 1} after ${delay}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw err;
}
}
throw lastError;
}Add timing instrumentation to identify which operations are causing timeouts:
async function timedOperation(name, operation, timeoutMs = 5000) {
const startTime = Date.now();
try {
const result = await timeout(timeoutMs, operation);
const duration = Date.now() - startTime;
console.log(`${name} completed in ${duration}ms`);
return result;
} catch (err) {
const duration = Date.now() - startTime;
console.error(`${name} failed after ${duration}ms:`, err.message);
if (err.message.includes('TimeoutError')) {
// Log to monitoring service
console.error('TIMEOUT:', {
operation: name,
duration,
configuredTimeout: timeoutMs,
timestamp: new Date().toISOString(),
});
}
throw err;
}
}
// Usage
await timedOperation('Database query', () => db.query('SELECT * FROM users'));Install and use the p-timeout library for production-ready timeout management:
npm install p-timeoutimport pTimeout from 'p-timeout';
try {
const result = await pTimeout(
fetch('https://api.example.com/data'),
{
milliseconds: 5000,
message: 'API request timed out',
fallback: () => {
// Optional fallback value when timeout occurs
return { cached: true, data: cachedData };
},
}
);
} catch (err) {
if (err.name === 'TimeoutError') {
console.error('Request exceeded 5 second limit');
}
}The library handles edge cases and provides clear error messages.
Important distinction: Adding a timeout to a promise does NOT terminate or cancel the underlying operation—it only stops waiting for it. The operation continues running in the background. For truly cancellable operations, you must use AbortController and ensure the underlying API supports abortion (like fetch, most HTTP libraries, and some database drivers).
When using Promise.race() for timeouts, both promises continue executing. The losing promise's result is discarded, but its side effects still occur. This can lead to unexpected behavior if you're not careful with resource cleanup.
For Node.js v15+, the timers/promises module provides native promise-based setTimeout:
import { setTimeout } from 'timers/promises';
try {
await setTimeout(5000, 'timeout', { signal: AbortSignal.timeout(3000) });
} catch (err) {
console.error('Aborted before completion');
}In microservices architectures, timeout errors often cascade. If Service A times out waiting for Service B, ensure Service A doesn't retry immediately—this can amplify load on Service B. Use circuit breakers (like opossum) to prevent cascading failures:
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(asyncOperation, {
timeout: 5000, // Timeout threshold
errorThresholdPercentage: 50,
resetTimeout: 30000, // Try again after 30s
});
breaker.fallback(() => ({ error: 'Service unavailable' }));For database operations, timeout errors may indicate missing indexes, N+1 query problems, or connection pool exhaustion. Use query explain plans and connection pool monitoring to diagnose the root cause rather than simply increasing timeout values.
Consider returning 408 Request Timeout HTTP status codes to clients when upstream operations time out, rather than generic 500 errors. This provides better semantic meaning and helps with debugging.
Error: Listener already called (once event already fired)
EventEmitter listener already called with once()
Error: EACCES: permission denied, open '/root/file.txt'
EACCES: permission denied
Error: Invalid encoding specified (stream encoding not supported)
How to fix Invalid encoding error in Node.js readable streams
Error: EINVAL: invalid argument, open
EINVAL: invalid argument, open
TypeError: readableLength must be a positive integer (stream config)
TypeError: readableLength must be a positive integer in Node.js streams