Node-API (N-API) is Bun's implementation of the Node.js C API for building native addons. It provides a stable ABI-compatible interface that allows native modules compiled as .node files to run without recompilation across different Node.js and Bun versions. This implementation enables compatibility with the vast ecosystem of Node.js native addons.
For information about other Node.js compatibility APIs, see Node.js Compatibility Layer.
N-API Architecture: The N-API layer bridges native C/C++ code with JavaScriptCore through a stable ABI. The NapiEnv structure maintains environment state, handle scopes manage value lifetimes, and finalizers integrate with garbage collection.
Sources: src/bun.js/bindings/napi.h171-481 src/napi/napi.zig5-70 src/bun.js/bindings/napi.cpp79-236
The napi_env type is a pointer to NapiEnv, the central structure that maintains all state for a native module's execution environment:
NapiEnv Structure: The environment maintains references to the global object, VM, finalizers for cleanup, cleanup hooks, pending exceptions, and module-specific data.
Key methods on NapiEnv:
checkGC() - Assert not currently in garbage collection (experimental modules only)doFinalizer() - Execute a finalizer callback, deferring if in GCcleanup() - Run all cleanup hooks and finalizers at environment destructionmustDeferFinalizers() - Returns true for non-experimental modules that can't run finalizers during GCSources: src/bun.js/bindings/napi.h171-481 src/napi/napi.zig5-70
napi_value is an opaque handle representing JavaScript values. In Bun's implementation, it's an enum(i64) that's ABI-compatible with JavaScriptCore's JSValue encoding:
napi_value Conversion: All napi_value operations automatically append values to the current handle scope to prevent premature garbage collection.
The key insight is that every time a napi_value is returned from Bun to native code, NapiHandleScope.append() is called to register it in the current handle scope.
Sources: src/napi/napi.zig131-151 src/bun.js/bindings/napi.cpp238-262
Handle scopes prevent JavaScript values from being garbage collected while native code is executing. Every N-API function that returns a napi_value must append it to the current handle scope:
Handle Scope Hierarchy: Handle scopes form a stack. When a scope is closed, all values in it (except escaped values) become eligible for garbage collection.
Key implementation details:
NapiHandleScope__open() creates a new scope, returning nullptr if unsafe (e.g., inside a finalizer)NapiHandleScope__append() adds a value to the current scopeNapiHandleScope__escape() moves a value to the parent scope's escape slotSources: src/napi/napi.zig89-122 src/bun.js/bindings/napi_handle_scope.h test/napi/napi-app/standalone_tests.cpp237-336
Native modules are loaded via dlopen (or LoadLibrary on Windows) and registered through the napi_module_register function:
Module Loading Process: Native modules are loaded through dlopen, registered via napi_module_register, and executed through executePendingNapiModule().
The module register function is called after dlopen completes, storing the module metadata in m_pendingNapiModule. Later, executePendingNapiModule() creates an environment and calls the module's init function with an exports object.
Sources: src/bun.js/bindings/napi.cpp726-808 src/bun.js/bindings/BunProcess.cpp (dlopen implementation)
N-API provides bidirectional conversion between JavaScript values and C types:
| N-API Type | JavaScript Type | Create Function | Get Function |
|---|---|---|---|
napi_undefined | undefined | napi_get_undefined | - |
napi_null | null | napi_get_null | - |
napi_boolean | boolean | napi_get_boolean | napi_get_value_bool |
napi_number | number | napi_create_int32/uint32/int64/double | napi_get_value_int32/uint32/int64/double |
napi_string | string | napi_create_string_utf8/latin1/utf16 | napi_get_value_string_utf8/latin1/utf16 |
napi_symbol | symbol | napi_create_symbol | - |
napi_object | object | napi_create_object | - |
napi_function | function | napi_create_function | napi_call_function |
napi_external | opaque | napi_create_external | napi_get_value_external |
napi_bigint | bigint | napi_create_bigint_words | napi_get_value_bigint_words |
String conversion functions handle different encodings:
napi_create_string_utf8 - Creates JavaScript string from UTF-8 bytesnapi_create_string_latin1 - Creates from Latin-1 (single byte per char)napi_create_string_utf16 - Creates from UTF-16 code unitsnapi_get_value_string_* - Copies JavaScript string to native buffer with specified encodingNumber conversions handle overflow and type coercion matching JavaScript semantics:
Sources: src/napi/napi.zig278-498 src/bun.js/bindings/napi.cpp348-422 test/napi/napi-app/conversion_tests.cpp
Finalizers are callbacks that run when JavaScript objects are garbage collected:
Finalizer Execution Model: Experimental modules (NAPI_VERSION_EXPERIMENTAL) run finalizers immediately during GC. Regular modules defer finalizers to the next tick to avoid affecting GC state.
Key concepts:
BoundFinalizer - Stores callback, hint, data, and active flagmustDeferFinalizers() - Returns false only for experimental modulesdoFinalizer() - Executes or defers a finalizer based on module versionDeferGCForAWhile - Prevents iterator invalidation during cleanupSources: src/bun.js/bindings/napi.h193-230 src/bun.js/bindings/napi.h332-344 src/bun.js/bindings/napi.h413-468
Cleanup hooks run when the environment is destroyed (process exit or module unload):
Cleanup Hook System: Hooks execute in reverse insertion order during environment cleanup. Duplicate hooks cause process abort (matching Node.js behavior).
The cleanup hook system uses NAPI_RELEASE_ASSERT to crash on duplicates, enforcing correctness like Node.js's CHECK_EQ macro.
Sources: src/bun.js/bindings/napi.h238-304 src/bun.js/bindings/napi.h481-530
AsyncContextFrame preserves async context (e.g., AsyncLocalStorage) across native boundaries:
Async Context Flow: When a callback is scheduled across a native boundary, it's wrapped in an AsyncContextFrame that captures the current context and restores it when the callback executes.
Key functions:
AsyncContextFrame::withAsyncContextIfNeeded() - Wraps callbacks if context tracking is enabledAsyncContextFrame::call() - Unwraps frame, sets context, calls callback, restores contextAsyncContextFrame::run() - Used when you have a specific frame to run inSources: src/bun.js/bindings/AsyncContextFrame.cpp37-166 src/bun.js/bindings/AsyncContextFrame.h1-66
N-API allows defining classes callable from JavaScript:
NapiClass Implementation: Native classes are JSFunction objects with custom call/construct behavior that invokes the native constructor callback.
Property descriptors support:
property.method callbackproperty.getter/property.setter callbacksproperty.value napi_valuenapi_static flagSources: src/bun.js/bindings/NapiClass.cpp1-139 src/bun.js/bindings/napi.cpp323-394 test/napi/napi-app/class_test.cpp1-216
napi_wrap attaches native data to JavaScript objects:
Object Wrapping Mechanism: Native data is stored in a NapiExternal attached to the object via a private symbol. For proxies, a WeakMap fallback ensures the wrap follows the proxy, not the target.
Key behaviors:
napi_remove_wrap clears the wrap without running finalizerSources: src/bun.js/bindings/napi_external.h test/napi/napi-app/wrap_tests.cpp test/napi/napi-app/module.js449-648
References allow native code to control object lifetime:
| Reference Type | Initial Refcount | Behavior |
|---|---|---|
| Weak | 0 | Object can be collected; ref becomes invalid |
| Strong | >0 | Object kept alive until refcount reaches 0 |
Reference Counting System: References use JSC's weak value mechanism with explicit refcounting. When refcount > 0, the weak value is kept alive.
Important: napi_reference_unref must not be called from finalizers in experimental modules (those with nm_version = NAPI_VERSION_EXPERIMENTAL). Regular modules can call it safely from finalizers.
Sources: src/bun.js/bindings/napi.cpp264-275 test/napi/napi-app/module.js551-641
Error Handling Flow: N-API functions return status codes and update m_lastNapiErrorInfo. Exceptions can be pending without throwing until explicitly checked.
Every N-API function call should use the pattern:
The NAPI_PREAMBLE macro in Bun's implementation automatically checks for pending exceptions and returns napi_pending_exception if one exists.
Sources: src/bun.js/bindings/napi.cpp163-236 src/bun.js/bindings/napi.h346-386 test/napi/napi-app/standalone_tests.cpp392-489
Thread-safe functions enable calling JavaScript from native threads:
Thread-Safe Function Lifecycle: Worker threads queue data via napi_call_threadsafe_function. The main thread's event loop drains the queue and invokes the callback with proper async context.
Key behaviors:
napi_tsfn_nonblocking returns napi_queue_full if queue is fullnapi_tsfn_blocking waits until space available (can deadlock if called from main thread)js_callback can be nullptr - only call_js_cb will be invokedSources: test/napi/napi-app/module.js73-78 test/napi/napi-app/standalone_tests.cpp31-89
The key difference between experimental and regular N-API modules:
| Module Type | nm_version | Finalizers | GC Checks | Use Case |
|---|---|---|---|---|
| Experimental | NAPI_VERSION_EXPERIMENTAL | Run during GC | Enforced | Modules that can't defer finalizers |
| Regular | <= 9 | Deferred to next tick | Not enforced | Most production modules |
GC Behavior by Module Type: Experimental modules must be extremely careful in finalizers - they cannot call most N-API functions. Regular modules have finalizers deferred, allowing more operations.
The DeferGCForAWhile scope used during cleanup prevents iterator invalidation when finalizers trigger additional GC during cleanup.
Sources: src/bun.js/bindings/napi.h306-344 src/bun.js/bindings/napi.h388-396 test/napi/napi-app/test_reference_unref_in_finalizer.c
The N-API test suite in test/napi/ includes:
test/napi/napi-app/ built with node-gypmodule.js provides JavaScript test harnessesnapi.test.ts spawns processes to test each scenarioTest categories:
Sources: test/napi/napi.test.ts1-648 test/napi/napi-app/module.js1-825 test/napi/napi-app/binding.gyp1-236
Refresh this wiki