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.
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.
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');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.
Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
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
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process