How to fix Warning: Memory leak detected. Unresolved promise prevents garbage collection in Node.js

Node.jsADVANCEDHIGHUpdated Dec 9, 2025· 162 views

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
);
  1. Add timeouts to all async operations
  2. Fix infinite promise chains
  3. Add comprehensive error handling
  4. Use AbortController for cancellable operations
node.js — error
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
);
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

  1. Promises created in loops or recursive functions that never resolve
  2. Missing error handling causing promises to remain pending on failures
  3. Infinite promise chains where each promise depends on the next
  4. Promise.race() or Promise.any() with promises that never settle
  5. Async operations without timeouts that hang indefinitely
  6. Retry logic that creates new promise chains without resolving old ones
  7. Event listeners or callbacks that create promises but never complete
  8. Network requests or I/O operations that timeout but promises remain pending

How to fix "Warning: Memory leak detected. Unresolved promise prevents garbage collection"

1Add timeouts to all async operations

Wrap promises with timeout logic to ensure they eventually settle:

javascript
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.

2Fix infinite promise chains

Break recursive promise chains by not returning promises in retry handlers:

javascript
// 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();
}
3Add comprehensive error handling

Ensure all promises have catch handlers to prevent them from hanging:

javascript
// 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);
}
4Use AbortController for cancellable operations

Implement proper cancellation to prevent orphaned promises:

javascript
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.

5Debug with heap snapshots

Identify unresolved promises using memory profiling:

javascript
// 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.

6Implement promise tracking in development

Track pending promises to identify leaks during development:

javascript
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.

Official resources & further reading