Troubleshooting
First identify the failing layer. A Rust compiler error, a generated-loader error, and a crash after a successful import have different owners and require different evidence.
| Failure point | Start with |
|---|---|
cargo or napi build exits non-zero |
Build failures |
require() / import cannot load the package |
Loader failures |
| Loader finds a binary but the OS rejects it | Binary and platform failures |
Runtime values work but .d.ts is wrong |
TypeScript generation |
| Promise hangs, process will not exit, worker crashes | Async and lifecycle failures |
| WASI/browser initialization fails | WASI failures |
Reduce the issue to one exported function and one plain Node script before adding a test runner, bundler, framework, or Electron. If the plain script works, the integration layer is part of the reproduction.
Capture the environment
Run these commands in the same shell, container, or CI job that fails:
node -p "process.version"
node -p "process.execPath"
node -p "process.platform + ' ' + process.arch"
node -p "JSON.stringify(process.versions, null, 2)"
rustc -vV
cargo -V
napi --version
On Linux, also record the runtime libc:
node -p "process.report?.getReport?.().header.glibcVersionRuntime || 'musl or unknown'"
ldd --version 2>&1 | head -1
Enable both CLI and Rust diagnostics:
DEBUG='napi:*' RUST_BACKTRACE=full napi build --platform --verbose
DEBUG='napi:*' RUST_BACKTRACE=full node ./repro.cjs
Keep the first error and its complete cause/backtrace. A later “build failed” line is usually only a summary.
Build failures
No crate found in manifest
--cwd is the base for every relative path. Verify what the CLI will read:
pwd
ls -l Cargo.toml package.json
cargo metadata --manifest-path Cargo.toml --format-version 1 --no-deps
In a split workspace, pass all paths explicitly. If the manifest is a virtual workspace, also pass the exact Cargo package name:
napi build \
--cwd packages/addon \
--manifest-path ../../Cargo.toml \
--package my-addon-native \
--package-json-path package.json \
--output-dir . \
--platform
See Manual setup for the meaning of each option.
Cargo succeeds but NAPI-RS cannot copy the artifact
Confirm that the selected package contains a cdylib target:
[lib]
crate-type = ["cdylib"]
Check whether CARGO_BUILD_TARGET_DIR, --target-dir, a custom Cargo profile,
or CARGO_BUILD_TARGET redirected Cargo output. Use the same --target and
--profile values for the build and copy step. DEBUG=napi:* prints the exact
source and destination paths used by the CLI.
A C/C++ dependency cannot find a compiler or library
Rust target installation is only one part of a native cross-build. Build
scripts for openssl-sys, ring, zstd-sys, and similar crates also need a C
compiler and libraries for the target. Do not point them at host libraries.
Use the cross-build decision matrix, then inspect the first
failing compiler invocation. Record CC, target-specific CC_*, linker, SDK,
sysroot, and pkg-config variables. For WASI C/C++ dependencies, configure
WASI_SDK_PATH as described in WebAssembly.
Loader failures
The generated loader records native-candidate load failures in an error cause
chain. Print it instead of reporting only “Cannot find native binding”:
try {
require('./index.js')
} catch (error) {
let current = error
let depth = 0
while (current) {
console.error(`[cause ${depth}]`, current.stack || current)
current = current.cause
depth += 1
}
process.exitCode = 1
}
The individual native causes distinguish a missing file from a wrong
architecture, missing shared library, or unsupported Node-API symbol. An
ordinary failed WASI fallback is not appended to this chain. To diagnose that
path explicitly, rerun with NAPI_RS_FORCE_WASI=error; the thrown error then
chains the WASI binding failures.
The optional platform package is missing
Record the detected platform and installed dependency tree:
node -p "process.platform + ' ' + process.arch"
npm ls your-package
find node_modules -type f \( -name '*.node' -o -name '*.wasm' \)
Common causes are:
- installation used
--no-optionalor omitted optional dependencies; - a lockfile generated on another platform did not include the current target;
- a deployment copied only production JavaScript and discarded
.nodefiles; - pnpm/Yarn supported-architecture settings exclude the deployment CPU/libc;
- the root and optional platform packages are different versions.
Set NAPI_RS_ENFORCE_VERSION_CHECK=1 to turn the last case into an explicit
version-mismatch error. If npm omitted an optional platform dependency because
of its lockfile behavior, remove both node_modules and the affected lockfile,
then install again on the target platform. Inspect or save the old lockfile
first when it is needed for a bug report.
Force one exact native library
To separate loader selection from binary loading, point the generated loader at one absolute path:
NAPI_RS_NATIVE_LIBRARY_PATH="$PWD/addon.linux-x64-gnu.node" node load-repro.cjs
If that succeeds, the binary is valid and normal platform/package selection is the failing layer. If it fails, the new cause is the operating system's direct loader error. Do not ship this environment variable as the normal package configuration.
CommonJS/ESM parse or export errors
- A CommonJS wrapper inside a
"type": "module"package must use.cjs. - Generate a real ESM wrapper with
napi build --platform --esmwhen consumers need static named ESM exports. - Importing a CommonJS wrapper through a transpiling test runner is not the same as testing with plain Node.
- Keep native packages external to server bundles.
See Integrations and bundlers for tested package shapes and externalization recipes.
Binary and platform failures
Inspect the actual file selected by the loader:
file ./addon.*.node
Then inspect dynamic dependencies:
# Linux
ldd ./addon.linux-x64-gnu.node
# macOS
otool -L ./addon.darwin-arm64.node
# Windows Developer Command Prompt
dumpbin /DEPENDENTS addon.win32-x64-msvc.node
Typical messages mean:
| Message | Likely cause |
|---|---|
wrong ELF class, Exec format error, not a valid Win32 application |
CPU or operating-system mismatch |
GLIBC_x.y not found |
Binary was built against newer glibc than the runtime |
lib*.so / .dylib / .dll not found |
A non-system native dependency was not shipped or its search path is wrong |
undefined symbol: napi_* |
Addon enabled a newer Node-API level than the runtime provides, or was linked/loaded incorrectly |
invalid ELF header while loading on Alpine |
glibc binary selected for a musl runtime, or the reverse |
Do not rename a musl binary to a gnu suffix or an x64 binary to an arm64
suffix. The suffix is a selection contract, not a conversion.
For GLIBC_x.y not found, rebuild against an older glibc as documented in
Cross build: glibc versions. Switching to a
musl target is only correct when the deployment actually uses musl.
TypeScript generation
If no .d.ts file is emitted, verify that the selected Cargo package directly
depends on napi-derive with its type-def feature. Default features include
it; disabling default features requires adding it back explicitly:
napi-derive = { version = "3", default-features = false, features = ["strict", "type-def"] }
If a declaration is missing or stale:
- Confirm the export is compiled for the current target and is not hidden by
#[cfg(...)]or#[napi(skip_typescript)]. - Confirm the CLI selected the intended Cargo package and
package.json. - Rebuild without watch mode and inspect
DEBUG=napi:*output. - Remove only the generated type-definition cache under
target/napi-rs, then rebuild. - Run
tsc --noEmitagainst the newly generated file.
Do not hand-edit generated declarations; the next build overwrites them. Use
ts_args_type, ts_return_type, dtsHeader, or a hand-written public wrapper
when the Rust-to-TypeScript mapping intentionally differs.
Async and lifecycle failures
A Promise never settles
Determine which abstraction owns it:
- Tokio
async fn: check for blocking work on the async runtime and detached tasks that never complete. AsyncTask: check whethercompute,resolve,reject, orfinallyis blocked.AbortSignalonly cancels work that has not started unless the task implements cooperative cancellation.- ThreadsafeFunction: handle
QueueFullandClosing; do not block while the JavaScript thread is waiting on the producer. - Stream/iterator: make cancellation wake the producer and close every sender.
Add timestamps and operation IDs on both sides of the boundary. A Rust log that says “queued” and a JavaScript log that says “awaiting” do not prove the completion callback ran.
Node does not exit
Move the reproduction to a child process with a deadline. Then look for:
- a strong ThreadsafeFunction that should have been weak;
- undropped ThreadsafeFunction clones or JavaScript references;
- Tokio tasks without an owner/shutdown path;
- workers, timers, streams, or sockets left open by either JavaScript or Rust.
Test the real exit path rather than calling process.exit(), which hides active
handles and skipped cleanup.
Worker termination crashes or hangs
Load the addon independently in every worker isolate. Do not share Env, class
constructors, or JavaScript handles globally between isolates. Implement a
graceful stop/cancel/await protocol before worker.terminate().
Abrupt termination during active native async work remains a runtime-sensitive limitation, with an open Bun report in napi-rs#2938. Reproduce in plain Node and the target Bun/Electron runtime separately.
See Async and concurrency and Testing and debugging for lifecycle tests.
Native panic or process abort
Run a debug build with RUST_BACKTRACE=full and attach a native debugger. A
panic cannot always be recovered safely across an FFI boundary. Convert
expected failures to napi::Result; reserve panics for violated internal
invariants, and document whether #[napi(catch_unwind)] is used.
Follow Testing and debugging for CodeLLDB, LLDB, GDB, worker stress, and leak tests.
WASI failures
SharedArrayBuffer is not defined or memory creation fails
The browser page is not cross-origin isolated. Serve the main document and subresources with:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Confirm in the browser console:
console.log(globalThis.crossOriginIsolated, typeof SharedArrayBuffer)
Also ensure cross-origin scripts, workers, WASM, and images satisfy the selected COEP policy; one blocked subresource can make an otherwise correct build fail.
The WASI optional package was not installed
WASI packages use cpu: ["wasm32"] and are skipped by default. Configure the
package manager's supported architectures or install with npm's --cpu=wasm32
as shown in WebAssembly: install the package.
With a loader generated by @napi-rs/cli 3.7 or newer:
NAPI_RS_FORCE_WASI=trueattempts the WASI path even when native loaded.NAPI_RS_FORCE_WASI=erroralso throws if no WASI binding can be found.1,0,false, and other strings do not force WASI.
Use error in tests so a missing WASI package cannot silently fall back to the
native addon.
Browser worker errors are invisible
Set napi.wasm.browser.errorEvent to true. The generated worker forwards an
error to the window as napi-rs-worker-error:
window.addEventListener('napi-rs-worker-error', (event) => {
console.error(event.detail)
})
Works in Node but not Bun or Deno
Do not assume Node's WASI implementation exists with the same API elsewhere. WASI execution in Bun and Deno has an open incompatibility report (napi-rs#2965). Mark the runtime unsupported or provide a separately tested loader until that product gap is resolved.
Report an actionable issue
Include:
- minimal Rust source,
Cargo.toml,build.rs,package.json, and JavaScript reproduction; - complete commands and the first error with its
causechain; - Node/runtime, CLI, Rust, host, target, CPU, and libc versions;
- whether plain Node works before a test runner or bundler is added;
- output of
fileand the platform dependency inspection command; - whether the artifact is debug/release, native/WASI, local/optional package;
- for lifecycle bugs, a bounded stress test and the exact shutdown sequence.
INFO
Remove credentials, absolute private paths, and proprietary input data, but do not remove the platform, target triple, or original operating-system loader message. Those details often identify the failing layer immediately.