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_contextnot 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()withnapi_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-hooksflags 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