Introduction

Node.js N-API native addons use napi_async_context to track asynchronous operations for proper async hooks, diagnostic reporting, and context propagation. When an async scope is opened with napi_open_async_context() but not closed with napi_close_async_context(), the async context leaks, causing memory growth, incorrect async hook behavior, and diagnostic data corruption. This error is particularly insidious because it does not crash the process immediately -- instead, it causes gradual memory growth and misleading performance profiles that are hard to attribute to the native addon.

Symptoms

Memory grows when using native addon:

```bash # Without native addon $ node -e "setInterval(() => {}, 1000)" RSS after 1 hour: 45MB

# With native addon that leaks async context $ node -e "const addon = require('./build/Release/addon'); addon.start()" RSS after 1 hour: 350MB ```

Or async hooks report incorrect data:

```javascript const { executionAsyncId } = require('async_hooks');

console.log(executionAsyncId()); // Returns stale async ID from the native addon's leaked context ```

Or diagnostic tools show anomalous async resource count:

```javascript const { createHook } = require('async_hooks'); let asyncResourceCount = 0;

createHook({ init(asyncId, type) { asyncResourceCount++; }, destroy(asyncId) { asyncResourceCount--; }, }).enable();

// asyncResourceCount keeps growing and never decreases ```

Common Causes

  • napi_close_async_context not called: Async context opened but never closed
  • Error path skips cleanup: Exception thrown between open and close
  • Async callback not matched with close: One open per callback, but close called fewer times
  • Addon unload does not clean up: napi_close_async_context not called in cleanup hook
  • Multiple async operations share one context: Each operation needs its own context
  • N-API version mismatch: Different N-API versions handle async context differently

Step-by-Step Fix

Step 1: Properly open and close async context

```c #include <node_api.h>

typedef struct { napi_async_context async_context; napi_ref callback_ref; napi_env env; // ... other data } addon_context;

napi_value StartAsyncWork(napi_env env, napi_callback_info info) { addon_context* context = malloc(sizeof(addon_context)); context->env = env;

// Open async context - MUST be matched with close napi_status status = napi_open_async_context( env, "my_addon_async_work", // Resource name NAPI_UNDEFINED_ID, // Resource ID (auto-generated) &context->async_context );

if (status != napi_ok) { napi_throw_error(env, NULL, "Failed to open async context"); free(context); return NULL; }

// Do async work... // When done, ALWAYS close the context napi_close_async_context(env, context->async_context); free(context);

return NULL; } ```

Step 2: Use cleanup hook for guaranteed cleanup

```c void cleanup_hook(void* data, napi_env env, void* finalize_hint) { addon_context* context = (addon_context*)data;

// Close async context if still open if (context->async_context != NULL) { napi_close_async_context(context->env, context->async_context); context->async_context = NULL; }

// Delete callback reference if (context->callback_ref != NULL) { napi_delete_reference(context->env, context->callback_ref); }

free(context); }

napi_value Init(napi_env env, napi_value exports) { addon_context* context = malloc(sizeof(addon_context)); context->async_context = NULL; context->callback_ref = NULL; context->env = env;

// Register cleanup hook napi_add_env_cleanup_hook(env, cleanup_hook, context);

// ... rest of initialization return exports; }

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) ```

Step 3: Track async operations with a counter

```c typedef struct { napi_env env; int active_async_ops; napi_async_context* contexts; // Array of active contexts uv_mutex_t mutex; } addon_state;

void async_work_complete(napi_env env, void* data) { addon_state* state = (addon_state*)data;

uv_mutex_lock(&state->mutex);

// Close the async context for this operation if (state->active_async_ops > 0) { state->active_async_ops--; napi_close_async_context(env, state->contexts[state->active_async_ops]); }

uv_mutex_unlock(&state->mutex); } ```

Prevention

  • Always pair napi_open_async_context() with napi_close_async_context()
  • Register a cleanup hook with napi_add_env_cleanup_hook() for guaranteed cleanup
  • Use a counter to track active async operations and verify it reaches zero
  • Add assertions that verify async context is not NULL before closing
  • Test native addons with --async-hooks flags to detect context leaks
  • Use Valgrind or AddressSanitizer to detect memory leaks in native code
  • Document the async lifecycle in the addon's README for contributors

Verification

After implementing the fix, verify native addon does not leak async contexts:

```javascript // Test script to detect async context leaks const { createHook } = require('async_hooks');

let asyncCount = 0; const hook = createHook({ init(asyncId, type, triggerAsyncId) { if (type.includes('napi') || type.includes('native')) { asyncCount++; console.log(Native async created: ${asyncId}, type: ${type}); } }, destroy(asyncId) { asyncCount--; } });

hook.enable();

// Load and use the native addon const addon = require('./build/Release/addon');

// Run operations for (let i = 0; i < 100; i++) { addon.doAsyncWork(); }

// Wait for completion setTimeout(() => { console.log(Final async count: ${asyncCount}); if (asyncCount !== 0) { console.error('LEAK DETECTED: Async contexts not cleaned up'); } else { console.log('No leaks detected'); } hook.disable(); }, 5000); ```

  • [Technical troubleshooting: Fix Axios Axios Bug Silent Failure When Data Getle](axios-axios-bug-silent-failure-when-data-getlength-fails-in-node-http-adapter)
  • [Fix Eaddrinuse Address Already In Use Port 3000 Issue in Node.js](eaddrinuse-address-already-in-use-port-3000)
  • [Fix Econnrefused Localhost Docker Startup Issue in Node.js](econnrefused-localhost-docker-startup)
  • [Fix Enoent No Such File Relative Path Require Issue in Node.js](enoent-no-such-file-relative-path-require)
  • [Fix Eventemitter Memory Leak Max Listeners Exceeded Issue in Node.js](eventemitter-memory-leak-max-listeners-exceeded)

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Fix Node.js napi_async_context Scope Leak in Native Addons", "description": "Resolve Node.js napi_async_context scope leaks in native addons with proper async scope management, cleanup hooks, and async operation tracking.", "url": "https://www.fixwikihub.com/nodejs-napi-async-context-scope-leak-fix", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-04-21T18:05:08.076Z", "dateModified": "2026-04-21T18:05:08.076Z" } </script>