How to fix Warning: Memory leak detected. Unresolved promise prevents garbage collection in Node.js
This warning occurs when promises remain unresolved indefinitely, preventing JavaScript's garbage collector from freeing memory. Long-lived or infinite promise chains accumulate in memory, eventually leading to performance degradation or out-of-memory crashes.
Quick fix
Fixes Warning: Memory leak detected. Unresolved promise prevents garbage collection
function promiseWithTimeout(promise, timeoutMs = 5000) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
)
]);
}
// Usage
const result = await promiseWithTimeout(
fetch('https://api.example.com/data'),
3000
);- Add timeouts to all async operations
- Fix infinite promise chains
- Add comprehensive error handling
- Use AbortController for cancellable operations
function promiseWithTimeout(promise, timeoutMs = 5000) {return Promise.race([promise,new Promise((_, reject) =>setTimeout(() => reject(new Error('Operation timed out')), timeoutMs))]);}// Usageconst result = await promiseWithTimeout(fetch('https://api.example.com/data'),3000);Warning: Memory leak detected. Unresolved promise prevents garbage collection
What this error means
This error indicates that one or more promises in your Node.js application have not been resolved or rejected, and the references to these promises are preventing the garbage collector from reclaiming memory. In JavaScript, promises maintain references to their callbacks (resolve/reject handlers) and any data captured in closures. When a promise never settles (remains in a pending state indefinitely), all these references stay in memory. If this pattern repeats—such as in loops, recursive chains, or long-running processes—the accumulated unresolved promises can consume significant memory. This is particularly problematic with infinite promise chains, where each promise depends on the next one's resolution. Since the Promises/A+ specification requires that promises in a chain cannot be garbage collected until the entire chain resolves, an infinite or very long chain will continuously grow in memory until the process crashes.
Typical symptoms
- Gradual memory increase over time when monitoring Node.js heap usage
- Application eventually crashes with "JavaScript heap out of memory" error
- Memory profiling tools show increasing promise-related allocations
- Performance degradation as memory pressure increases
- Warning messages about possible EventEmitter memory leaks
- Process becomes unresponsive before crashing
Common causes
- Promises created in loops or recursive functions that never resolve
- Missing error handling causing promises to remain pending on failures
- Infinite promise chains where each promise depends on the next
- Promise.race() or Promise.any() with promises that never settle
- Async operations without timeouts that hang indefinitely
- Retry logic that creates new promise chains without resolving old ones
- Event listeners or callbacks that create promises but never complete
- Network requests or I/O operations that timeout but promises remain pending
How to fix "Warning: Memory leak detected. Unresolved promise prevents garbage collection"
Wrap promises with timeout logic to ensure they eventually settle:
function promiseWithTimeout(promise, timeoutMs = 5000) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
)
]);
}
// Usage
const result = await promiseWithTimeout(
fetch('https://api.example.com/data'),
3000
);This ensures that even if the underlying operation hangs, the promise will reject after the timeout period, allowing garbage collection.
Break recursive promise chains by not returning promises in retry handlers:
// BAD: Creates infinite chain
async function retryForever() {
try {
return await riskyOperation();
} catch (error) {
return retryForever(); // Chain grows indefinitely
}
}
// GOOD: Break the chain
function retryWithLimit(maxRetries = 5) {
let attempts = 0;
function attempt() {
return riskyOperation().catch(error => {
attempts++;
if (attempts >= maxRetries) {
throw error;
}
// Don't return - creates new chain instead
return new Promise(resolve =>
setTimeout(() => resolve(attempt()), 1000)
);
});
}
return attempt();
}Ensure all promises have catch handlers to prevent them from hanging:
// BAD: Missing catch can leave promise pending
async function processData(items) {
const promises = items.map(item => saveToDatabase(item));
return Promise.all(promises);
}
// GOOD: Handle errors to ensure settlement
async function processData(items) {
const promises = items.map(item =>
saveToDatabase(item).catch(error => {
console.error(`Failed to save item ${item.id}:`, error);
return null; // Resolve with null instead of hanging
})
);
const results = await Promise.all(promises);
return results.filter(r => r !== null);
}Implement proper cancellation to prevent orphaned promises:
const controller = new AbortController();
const signal = controller.signal;
// Set timeout
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://api.example.com/data', { signal });
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else {
throw error;
}
}This allows you to cancel pending operations and let their promises settle.
Identify unresolved promises using memory profiling:
// Add to your application for debugging
const v8 = require('v8');
const fs = require('fs');
function takeHeapSnapshot() {
const filename = `heap-${Date.now()}.heapsnapshot`;
const heapSnapshot = v8.writeHeapSnapshot(filename);
console.log('Heap snapshot written to:', heapSnapshot);
}
// Take snapshots before and after suspected leak
takeHeapSnapshot();
// ... run your code
setTimeout(takeHeapSnapshot, 60000);Compare snapshots in Chrome DevTools to identify growing promise references.
Track pending promises to identify leaks during development:
const pendingPromises = new Set();
function trackPromise(promise, label) {
const tracked = { promise, label, created: Date.now() };
pendingPromises.add(tracked);
promise.finally(() => {
pendingPromises.delete(tracked);
});
return promise;
}
// Periodically check for old promises
setInterval(() => {
const now = Date.now();
pendingPromises.forEach(p => {
const age = now - p.created;
if (age > 10000) { // 10 seconds
console.warn(`Long-pending promise: ${p.label} (${age}ms old)`);
}
});
}, 5000);
// Usage
await trackPromise(fetchData(), 'User data fetch');Advanced notes
Promise.race() Memory Considerations:
Be cautious with Promise.race() on arrays of promises where some may never resolve. The "losing" promises remain in memory even after the race completes. Consider cleaning up references or using AbortController to cancel the losing operations.
Event Loop and Promise Queuing:
Unresolved promises don't just consume heap memory—they also keep references in the microtask queue. Understanding Node.js's event loop phases helps identify where promises might be stalling. Tools like --trace-promises can help debug promise resolution order.
WeakRef and FinalizationRegistry:
In modern Node.js versions (14.6+), you can use WeakRef and FinalizationRegistry to detect when promise-related objects are garbage collected, helping identify which promises are preventing cleanup.
Production Monitoring:
Implement metrics for promise queue depth and long-pending operations. Tools like prom-client can expose Prometheus metrics for promise pool sizes, and APM tools like New Relic or DataDog can track async operation lifecycles.
Cluster Mode Implications:
In cluster mode, each worker process has its own heap. A promise memory leak in one worker won't directly affect others, but the pattern will likely repeat across all workers, amplifying the memory consumption.
Frequently asked questions
- What does "Warning: Memory leak detected. Unresolved promise prevents garbage col" mean?
- js application have not been resolved or rejected, and the references to these promises are preventing the garbage collector from reclaiming memory. In JavaScript, promises maintain references to their callbacks (resolve/reject handlers) and any data captured in closures. When a promise never settles (remains in a pending state indefinitely), all these references stay in memory.
- How do I fix "Warning: Memory leak detected. Unresolved promise prevents garbage col"?
- This warning occurs when promises remain unresolved indefinitely, preventing JavaScript's garbage collector from freeing memory. Long-lived or infinite promise chains accumulate in memory, eventually leading to performance degradation or out-of-memory crashes.
- What causes "Warning: Memory leak detected. Unresolved promise prevents garbage col"?
- Promises created in loops or recursive functions that never resolve Missing error handling causing promises to remain pending on failures Infinite promise chains where each promise depends on the next
- Is the "Warning: Memory leak detected. Unresolved promise prevents garbage col" error serious?
- Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.