How to fix TimeoutError: Promise.race() timed out (first promise never settled) in Node.js
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.
Quick fix
Fixes TimeoutError
// 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
]);- Review the timeout duration and operation complexity
- Implement proper timeout cleanup to prevent memory leaks
- Add distinguishable timeout errors for better handling
- Debug why the original promise never settles
// Example timeout patternconst 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 itconst result = await Promise.race([someSlowOperation(),timeout(1000) // May be too short]);TimeoutError: Promise.race() timed out (first promise never settled)
What this error means
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.
Typical symptoms
- Operations fail with "TimeoutError" after a specified duration
- Promise.race() rejects before the main promise completes
- Network requests or database queries appear to hang
- Node.js process does not exit cleanly due to pending promises
- Memory usage increases over time from unresolved promises
- Timeout handlers fire but the original operation continues in background
Common causes
- Network request takes longer than the configured timeout
- Database query is slow or unresponsive
- External API is not responding or experiencing latency
- Promise never resolves or rejects due to missing error handling
- Timeout duration is set too low for the operation
- Deadlock or circular dependency preventing promise settlement
- Event listener or callback never fires to settle the promise
- Resource contention causing operation to stall
How to fix "TimeoutError: Promise.race() timed out (first promise never settled)"
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.
Advanced notes
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.
Frequently asked questions
- What does "TimeoutError" mean?
- race() to implement a timeout mechanism, but the original promise being raced never settles (neither resolves nor rejects). 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. race() to race the actual operation against a timeout promise.
- How do I fix "TimeoutError"?
- 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.
- What causes "TimeoutError"?
- Network request takes longer than the configured timeout Database query is slow or unresponsive External API is not responding or experiencing latency
- Is the "TimeoutError" error serious?
- It is a medium-severity error โ more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- ๐Node.js Official Documentation
- ๐MDN Web Docs: Promise.race()(Official Docs)
- ๐
- ๐
- ๐