This error occurs when implementing a timeout pattern with Promise.race() where the racing promise never resolves or rejects within the specified timeout period. The timeout promise wins the race, but the original promise remains pending indefinitely.
This error happens when you use Promise.race() to implement a timeout mechanism, but the original promise being raced never settles (neither resolves nor rejects). Promise.race() returns a promise that settles with the first promise to complete, so when a timeout promise rejects before the main promise settles, you get a TimeoutError. The core issue is that JavaScript promises do not have built-in timeout support, so developers commonly use Promise.race() to race the actual operation against a timeout promise. When the timeout promise rejects first (because the main operation is taking too long), the race is won by the timeout, and a TimeoutError is thrown. It is important to understand that Promise.race() does not cancel or terminate the losing promise - it continues running in the background. This can lead to memory leaks, hanging processes, or unexpected side effects if the original promise eventually settles after the timeout.
First, verify if your timeout is reasonable for the operation being performed. Check your timeout implementation:
// Example timeout pattern
const timeout = (ms: number) => {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operation timed out')), ms)
);
};
// If timing out at 1000ms for a complex operation, increase it
const result = await Promise.race([
someSlowOperation(),
timeout(1000) // May be too short
]);Consider increasing the timeout if the operation legitimately needs more time.
Add cleanup logic to clear the timeout timer when the race completes:
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<T>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error('Operation timed out after ' + ms + 'ms'));
}, ms);
});
return Promise.race([promise, timeoutPromise])
.finally(() => clearTimeout(timeoutId)); // Clean up timer
}
// Usage
try {
const data = await withTimeout(fetch('https://api.example.com'), 5000);
console.log('Success:', data);
} catch (error) {
console.error('Request failed or timed out:', error);
}This ensures the timeout timer is cleared whether the promise resolves, rejects, or times out.
Create a custom TimeoutError class to differentiate timeout failures from other errors:
class TimeoutError extends Error {
constructor(message: string) {
super(message);
this.name = 'TimeoutError';
}
}
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<T>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new TimeoutError('Promise timed out after ' + ms + 'ms'));
}, ms);
});
return Promise.race([promise, timeoutPromise])
.finally(() => clearTimeout(timeoutId));
}
// Handle timeout errors specifically
try {
await withTimeout(longRunningTask(), 3000);
} catch (error) {
if (error instanceof TimeoutError) {
console.log('Operation timed out, retrying with longer timeout...');
await withTimeout(longRunningTask(), 10000);
} else {
console.error('Operation failed:', error);
}
}If timeouts persist, investigate why your promise is not settling:
// Add logging to track promise lifecycle
function debugPromise<T>(promise: Promise<T>, label: string): Promise<T> {
console.log('[' + label + '] Promise created');
return promise
.then((result) => {
console.log('[' + label + '] Promise resolved', result);
return result;
})
.catch((error) => {
console.log('[' + label + '] Promise rejected', error);
throw error;
});
}
// Use with timeout
const result = await withTimeout(
debugPromise(fetchData(), 'API Call'),
5000
);Check for:
- Missing resolve/reject calls in custom promises
- Event listeners that never fire
- Network connectivity issues
- Database connection problems
- Deadlocks or circular dependencies
Verify that all promises in your code have catch handlers to prevent silent failures:
// Bad: Promise may never settle if error occurs
const promise = new Promise((resolve, reject) => {
someAsyncOperation((error, result) => {
if (result) resolve(result);
// Missing: if (error) reject(error);
});
});
// Good: Proper error handling
const promise = new Promise((resolve, reject) => {
someAsyncOperation((error, result) => {
if (error) return reject(error);
resolve(result);
});
});
// Always add catch handlers
promise
.then(handleSuccess)
.catch(handleError); // Prevents unhandled rejectionFor operations that support cancellation (like fetch), use AbortController:
async function fetchWithTimeout(url: string, ms: number) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new TimeoutError('Request to ' + url + ' timed out after ' + ms + 'ms');
}
throw error;
}
}
// Usage
try {
const response = await fetchWithTimeout('https://api.example.com/data', 5000);
const data = await response.json();
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Request timed out');
}
}This actually cancels the network request instead of just ignoring it.
Memory Leak Prevention: When using Promise.race() for timeouts, remember that losing promises continue executing in the background. This can cause memory leaks if you are creating many timed operations. Always use clearTimeout() in a finally block to clean up timeout timers.
Promise.race() Behavior: Promise.race() never settles synchronously, even with an empty array. If passed an empty iterable, the returned promise remains pending forever. With a non-empty iterable, it settles as soon as the first promise settles.
Process Hanging: In Node.js, unresolved promises can prevent the process from exiting cleanly. This happens because the event loop keeps running while waiting for pending promises. Use proper cleanup and consider tools like why-is-node-running to debug processes that will not exit.
Alternative Patterns: For operations that support cancellation (like fetch with AbortController), prefer actual cancellation over Promise.race() timeouts. This stops the operation rather than just ignoring its result.
Promise.allSettled() Alternative: If you need to wait for multiple operations with a timeout but do not want to lose results, consider using Promise.allSettled() with a timeout wrapper instead of Promise.race(). This lets you see which promises completed and which timed out.
Debugging Tools: Use Node.js --trace-warnings flag to track unhandled promise rejections. For persistent issues, libraries like p-timeout (npm) provide production-ready timeout implementations with better error handling.
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