Debugging Wasm in Node.js with the Inspector

This page answers one task: step through a WebAssembly module that runs in Node — in a server, a CLI tool or a test suite — with the same breakpoints, call stacks and variable views you would have in a browser.

Prerequisites

  • [ ] Node 20 or newer.
  • [ ] A module built with names, and with DWARF if you want source-level stepping.
  • [ ] Chrome or VS Code to attach as the debugger front end.

Node exposes the same debugger as Chrome

Node runs JavaScript and WebAssembly on V8, the engine inside Chrome, and it exposes V8’s debugging interface through the inspector protocol. Starting Node with --inspect opens a WebSocket that any inspector client can connect to: Chrome DevTools, VS Code, or other tools. Because the engine is the same, everything that works for Wasm in Chrome’s Sources panel works for Wasm in Node — Wasm frames in the call stack, breakpoints in disassembly, and source-level debugging when DWARF is present and the client supports it.

What changes is how execution starts. In a browser you load a page and then interact with it; a Node process starts running immediately and may finish before you can attach. That is why the inspector has a variant that pauses on the first line, and why test runners need their own flags to keep the process alive long enough to debug.

Attaching a debugger to a Node process running Wasm Node starts with --inspect-brk and pauses before running any user code. The debugger client connects over the inspector WebSocket, sets breakpoints in the Wasm module's source files, and resumes. When the export is called, V8 pauses at the breakpoint and reports Wasm and JavaScript frames to the client. node --inspect-brk inspector (V8) DevTools / VS Code listen on ws://127.0.0.1:9229, pause at start attach and set breakpoints resume module instantiated; export called paused: Wasm frame + locals

Step 1 — start Node with the inspector

node --inspect-brk scripts/run.mjs
Debugger listening on ws://127.0.0.1:9229/9f4c3b1e-6f1d-4a9e-a3f1-2f8d7c1e0b42
For help, see: https://nodejs.org/en/docs/inspector

--inspect-brk pauses before the first line of user code, giving you time to attach and set breakpoints. Use --inspect without -brk for long-running servers where you attach later. Bind to localhost only — the inspector allows arbitrary code execution, and exposing it on a network interface is a security hole.

Step 2 — attach Chrome DevTools

Open chrome://inspect in Chrome, find the Node target under Remote Target, and click inspect. A dedicated DevTools window opens, paused at the start of your script. In its Sources panel, the module appears under wasm:// once instantiated; with DWARF and the C/C++ DevTools Support extension installed, its original source files appear too. Set breakpoints, then resume.

For a script that instantiates the module after some setup, it is often easiest to add a debugger; statement just before the first call into the module, resume to it, and set Wasm breakpoints from there, once the module is loaded and visible.

Step 3 — or attach VS Code

A launch configuration starts Node under the debugger directly, with breakpoints set in the editor:

{
  "type": "node",
  "request": "launch",
  "name": "Debug Wasm in Node",
  "program": "${workspaceFolder}/scripts/run.mjs",
  "enableDWARF": true,
  "skipFiles": ["<node_internals>/**"]
}

enableDWARF hands the module’s debug sections to the WebAssembly DWARF Debugging extension, so breakpoints set in .rs or .c files bind to the module. The editor-side setup, including path mapping for builds made elsewhere, is in debugging Wasm from VS Code.

Step 4 — debug a test

Test runners start worker processes, run quickly, and exit — all of which work against attaching a debugger. Run the tests in a single process with the inspector and no timeout:

# Vitest: no worker isolation, one thread, wait for the debugger
node --inspect-brk node_modules/vitest/vitest.mjs run --pool=forks --poolOptions.forks.singleFork --test-timeout=0 geometry.test.js

# Node's built-in test runner
node --inspect-brk --test test/geometry.test.mjs

Set a breakpoint in the Wasm source or add debugger; in the test before the failing call. Running only the failing test file keeps the session short.

Step 5 — debug a WASI module

A WASI module run by node:wasi is debugged the same way: the module is instantiated by your JavaScript host, so it appears in the inspector like any other. Breakpoints in the module’s source work once it is loaded:

// scripts/run-wasi.mjs
import { readFile } from "node:fs/promises";
import { WASI } from "node:wasi";

const wasi = new WASI({ version: "preview1", args: ["tool", "input.csv"], preopens: { ".": "." } });
const module = await WebAssembly.compile(await readFile("target/wasm32-wasip1/debug/tool.wasm"));
const instance = await WebAssembly.instantiate(module, wasi.getImportObject());
debugger;                         // set Wasm breakpoints now, then continue
wasi.start(instance);
node --inspect-brk scripts/run-wasi.mjs

The WASI side of this setup is covered in running WASI modules in Node.js.

Inspector flags for each way of running Wasm in Node Which Node inspector setup suits a one-shot script, a long-running server, a test suite and a WASI program. scenario flag tip one-shot script --inspect-brk add debugger; before the first call long-running server --inspect attach later from chrome://inspect test suite --inspect-brk + single fork disable timeouts WASI program --inspect-brk break after instantiate, before start

Making the module debuggable by default in development

Most friction in Node-side Wasm debugging comes from builds that were never meant to be debugged: stripped names, no DWARF, high optimization. It pays to make the development build debuggable by default and keep release settings for release. For Rust, a dev profile with opt-level = 1, debug = true, and wasm-pack’s --dev mode gives names, DWARF and code fast enough to run real workloads. For Emscripten, -O1 -g does the same. Put that in the project’s standard dev command so nobody has to remember flags when a bug arrives.

It also helps to keep a tiny “repro runner”: a script that loads the module, feeds it one input file named on the command line, and calls the failing entry point. Bugs reported from production can then be reproduced by saving the input and running one command under --inspect-brk, without starting the whole server or test suite. That turns a vague report into a paused debugger at the right function within minutes, and the input file becomes the regression test once the bug is fixed.

Finally, remember that server-side modules often run with different imports and configuration from browser ones — a WASI file system instead of fetch, environment variables instead of URL parameters. Reproduce with the same host setup as the failing environment, or the debugger will faithfully show you a different program.

CPU profiles from the inspector

The same connection records CPU profiles. In the attached DevTools, open the Performance or Profiler panel and record while the workload runs; Wasm functions appear by name. For a profile without attaching anything, Node writes one directly:

node --cpu-prof --cpu-prof-dir=profiles scripts/run.mjs

The resulting .cpuprofile file opens in Chrome DevTools’ Performance panel by dragging it in, and converts to flame graphs as described in generating flame graphs for Wasm. For the lowest-overhead, function-level view of server-side modules under wasmtime rather than Node, Linux perf is the better tool, as in profiling Wasm hot paths with perf.

Expected output

Paused inside a Wasm function called from a Node script, the Call Stack panel shows:

parse_record     src/parser.rs:118
parse_all        src/parser.rs:64
(anonymous)      app_bg.wasm
main             scripts/run.mjs:22

and the Scope panel lists the function’s variables with their Rust types.

Gotchas

  • The process exits before you attach. Use --inspect-brk, or add debugger; and start with --inspect-brk so it waits.
  • The module does not appear in Sources. It has not been instantiated yet. Break after instantiation.
  • Breakpoints in .rs files stay unbound. No DWARF in the build, or no DWARF-capable client. Build with debug info and install the extension.
  • Source paths do not match the workspace. The module was built in a container or CI with different paths. Map them in the client’s path overrides rather than rebuilding.
  • Port 9229 already in use. Another Node process is being inspected. Use --inspect-brk=0 for a random port, or stop the other process.

Performance note

The inspector costs little when attached but idle; breakpoints and stepping obviously stop execution. CPU profiling with --cpu-prof slowed a parsing workload by about 5%, small enough to profile realistic runs. A debug build is a different matter — the unoptimized module ran the same workload 3.4 times slower — so profile optimized builds with names kept.

Cost of debugging and profiling setups in Node Run time of a parsing workload in Node with an optimized build alone, with the inspector attached but idle, with --cpu-prof, and with an unoptimized debug build. seconds for the workload optimized, no inspector 1 s optimized, inspector attached 1.0 s optimized, --cpu-prof 1.1 s debug build (opt-level 0) 3.4 s

Frequently Asked Questions

Can I debug Wasm in Deno or Bun the same way? Deno supports --inspect and --inspect-brk with the same protocol. Bun’s inspector uses a different protocol and its own debugger front end; Wasm support there is less mature.

Does node inspect (the command-line debugger) work for Wasm? It can step and set breakpoints, but has no source-level Wasm support. A graphical client is far more practical.

Can I attach to a production server? Technically yes, but pausing a server stops every request. Use profiles and logs in production and reproduce bugs locally for stepping.

Can I debug a module compiled by another process? The inspector debugs what runs inside the Node process. If a module is precompiled elsewhere and loaded as bytes, it still appears once instantiated; DWARF must be present in those bytes for source-level stepping.

What about worker threads? Each worker_threads worker appears as a separate target. Attach to the worker that runs the module.

← Back to Debugging & Profiling Wasm Modules