Node.js Promise内存泄漏排查:追踪未完成Promise的方法
Great question—tracking pending Promise leaks in Node.js 8.x can be tricky, but there are several targeted tools and techniques to get to the root of it. Here's how I'd approach this:
Since you're already using Chrome's allocation timeline, you can dig deeper into those leaked Promise objects directly:
- When you capture a heap snapshot or allocation timeline entry for a pending Promise, right-click the object and select Show retainers to see exactly what's keeping it in memory (e.g., a global cache, an event listener, or an uncompleted async context).
- Enable Async stack traces in DevTools (check
Settings > Experimentsif it's hidden) to get full call stacks for async operations. This will show you the exact line of code where the Promise was created, even across async boundaries. - Filter heap snapshots by
Promiseand sort byPromiseStateto isolate all pending instances. For each, inspect the[[PromiseResult]]and[[PromiseFulfillReactions]]/[[PromiseRejectReactions]]properties—empty reactions often mean the Promise was never resolved/rejected.
Sometimes DevTools isn't enough to pinpoint creation context, so add manual tracing to your code:
- Wrap your Promise or async function calls to attach a creation stack trace:
function trackedAsync(fn) { return async function(...args) { const promise = fn(...args); // Attach stack trace for later inspection promise._createdAt = new Error().stack; return promise; }; } // Use it like this: const myLeakyFunction = trackedAsync(async () => { // Your async logic here }); - In heap snapshots, search for Promises with the
_createdAtproperty—this will give you the exact call stack where the Promise was spawned, even if it's still pending.
Once you've found the source of pending Promises, check these common culprits:
- Unresolved async dependencies: The Promise is waiting on an operation that never completes—like a TCP connection that hangs, a file stream that never closes, or an event that's never triggered.
- Global or long-lived references: The Promise (or its parent context) is stored in a global array, cache, or class static property that's never cleared.
- Forgotten event listeners: If your async function binds an event listener (e.g.,
process.on('data')) but never removes it, the listener holds a reference to the Promise's context, preventing GC. - Swallowed rejections: Even rejected Promises can leak if their errors are never caught (Node.js 8.x will warn about unhandled rejections, but use
--trace-warningsto get full stacks for these).
Node.js 8.x has built-in features to track async lifecycle:
async_hooksmodule: Use this to monitor Promise creation and destruction. You can log when Promises are initialized and destroyed—if a Promise never triggers thedestroyhook, it's likely leaked:const async_hooks = require('async_hooks'); const hook = async_hooks.createHook({ init(asyncId, type) { if (type === 'PROMISE') { console.log(`Promise ${asyncId} created`); // Store the asyncId and associated stack if needed } }, destroy(asyncId) { console.log(`Promise ${asyncId} destroyed`); } }); hook.enable();- Start Node.js with
--inspect: This lets you connect Chrome DevTools directly to your Node process, making it easier to capture heap snapshots while the app is running and inspect live Promise states. --trace-warningsflag: Run your app withnode --trace-warnings app.jsto get detailed stacks for unhandled Promise rejections, which can lead to leaked contexts.
By combining these techniques, you should be able to track down exactly which Promises are leaking, where they're created, and why they're never completing.
内容的提问来源于stack exchange,提问作者Petr

