Debugging Wasm from VS Code

This page answers one task: debug a WebAssembly module at the source level — Rust or C/C++ lines, local variables, call stacks — from inside VS Code, while the module runs in Chrome or Node.

Prerequisites

  • [ ] VS Code 1.90+ with the built-in JavaScript debugger (no extension needed for the JS side).
  • [ ] The WebAssembly DWARF Debugging extension (ms-vscode.wasm-dwarf-debugging) for source-level Wasm.
  • [ ] A debug build of your module with DWARF info: wasm-pack build --dev, cargo build without --release, or emcc -g.
  • [ ] Chrome or Edge, or Node 20+, as the runtime.

How the pieces fit together

VS Code does not run WebAssembly. It attaches to a runtime that does — Chrome through the DevTools protocol, or Node through its inspector — and asks it where execution is. The runtime answers in WebAssembly terms: function index 42, byte offset 0x1a3f in the code section. Turning that into “src/lib.rs line 88” requires the DWARF debug information the compiler embedded in the module, and a component that can read it.

That component is the DWARF extension. It reads the module’s custom debug sections, maps instruction offsets to source lines, and describes local variables in terms of the source language’s types. The JavaScript debugger hosts it, so a single debug session can step from JavaScript into a Wasm export and out again, with source-level views on both sides.

From a breakpoint in lib.rs to a paused module VS Code sends the breakpoint to the JavaScript debugger, the DWARF extension converts the source line into a Wasm code offset using the module's debug sections, the debugger sets the breakpoint in Chrome or Node through the inspector protocol, and when it is hit the reverse mapping shows source and variables. breakpoint in lib.rs line 88 DWARF extension line → code offset JS debugger CDP / inspector Chrome or Node pauses at 0x1a3f source view locals as Rust types

Step 1 — build with debug information that survives

For Rust with wasm-pack, the dev profile includes DWARF, and wasm-pack keeps it when you pass --dev. Make sure nothing in the pipeline strips it:

wasm-pack build --dev --target web
wasm-objdump -h pkg/app_bg.wasm | grep -E 'debug_(info|line)'
  Custom start=0x0004a1c2 end=0x0009b211 (size=0x0005104f) ".debug_info"
  Custom start=0x000a7e03 end=0x000c1188 (size=0x00019385) ".debug_line"

If the sections are missing, wasm-opt probably ran with --strip-debug, or wasm-bindgen ran without --keep-debug. For C and C++, compile and link with -g, and add -gseparate-dwarf=app.debug.wasm if the debug module is large — the DWARF extension follows the reference to the separate file. Debug information can be several times larger than the code, which is why release builds drop it; more on the format in debugging Wasm with DWARF and source maps.

Step 2 — a launch configuration for Chrome

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Debug Wasm in Chrome",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}/web",
      "enableDWARF": true,
      "sourceMapPathOverrides": {
        "/rustc/*": "${env:HOME}/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/*"
      }
    }
  ]
}

Start your dev server first, then press F5. VS Code launches a fresh Chrome profile attached to the debugger. Open src/lib.rs, click in the gutter, and trigger the code path from the page. Execution pauses on the Rust line, the Variables panel shows locals with their Rust types, and the Call Stack shows Rust frames above the JavaScript frame that called the export.

enableDWARF is on by default in recent releases but worth stating, since it is the switch that hands DWARF sections to the extension. The sourceMapPathOverrides entry is optional: it maps paths inside the standard library to your local copy, so stepping into Vec::push shows real source instead of an error.

Step 3 — a launch configuration for Node

Server-side and test code often runs the module in Node. The same extension handles it:

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

For a WASI module run by Node’s node:wasi, point program at the JavaScript host that instantiates it. For a module run by wasmtime, VS Code needs a native debugger (LLDB with wasmtime’s JIT debugging support) instead — a different workflow, covered with the runtime in debugging Wasm in Node.js with the Inspector.

Step 4 — fix path mapping when breakpoints stay grey

A breakpoint that remains hollow or grey means VS Code could not map the source file you clicked to any code in the loaded module. Nine times out of ten the cause is paths. DWARF records the paths the compiler saw, and if the build ran in a container, on CI, or with --remap-path-prefix, those paths do not match your workspace.

Check what the module recorded:

llvm-dwarfdump --debug-line pkg/app_bg.wasm | grep -m5 'include_directories\|file_names'
include_directories[  0] = "/build"
file_names[  1]:
           name: "src/lib.rs"
      dir_index: 0

Here the build remapped the workspace to /build. Tell the debugger how to translate it back:

"sourceMapPathOverrides": { "/build/*": "${workspaceFolder}/*" }

The reproducible-build settings that cause this are deliberate — see producing reproducible Wasm binaries — so the fix belongs in the launch configuration, not in the build.

Why a Wasm breakpoint stays grey A decision tree for an unbound breakpoint. If the module has no debug sections, rebuild with DWARF. If it has them but the recorded paths differ from the workspace, add a path override. If paths match, the code may have been inlined or optimized away. Breakpoint in source stays grey after the page loads no .debug_* sections Rebuild with DWARF --dev, -g, --keep-debug sections present, paths differ sourceMapPathOverrides map /build/* to workspace paths match Code optimized away lower opt-level for this crate

Step 5 — inspect memory and values the debugger cannot type

The Variables panel shows locals whose types DWARF describes. For raw buffers in linear memory — a pointer into an image, a byte slice handed over from JavaScript — use the debug console. Evaluate expressions against the module’s memory export while paused:

// in the Debug Console, while paused inside a Wasm frame
new Uint8Array(memories[0].buffer, 1052672, 32)

The exact name of the memory object in scope depends on the extension version; the Variables panel’s Module scope lists it. For a fuller memory viewer, DevTools’ memory inspector is still better than VS Code’s — see inspecting Wasm memory in Chrome DevTools.

A debugging session, start to finish

It helps to see the workflow as a whole rather than as configuration. Suppose a polygon-area function returns a negative number for some inputs. Start the dev server and the watcher, press F5 to launch the attached browser, and reproduce the bug in the page. Set a breakpoint on the line in geometry.rs that accumulates the area, and trigger the calculation again. When execution pauses, the Variables panel shows the loop index, the running sum and the two points being processed, as Rust values — not as raw memory.

Step over a few iterations and watch the sum. When it goes wrong, add a conditional breakpoint — right-click the gutter, Edit Breakpoint, and enter an expression such as i == 17 — so the next run stops exactly at the bad iteration without stepping through the first sixteen. Conditional breakpoints evaluate in the context of the paused Wasm frame, using the same DWARF information that powers the Variables panel.

Once you have a fix, edit the Rust file and save. The watcher rebuilds, the page reloads, and the breakpoints you set are re-bound against the new module automatically because they are attached to source lines, not to code offsets. That last property is what makes source-level debugging worth the setup: you can keep the same breakpoints across dozens of rebuilds while iterating on a fix.

Expected output

When the breakpoint hits, the Call Stack panel shows a mix of languages:

app::geometry::polygon_area          src/geometry.rs:88
app::__wasm_bindgen_generated_area   src/lib.rs:31
area                                 pkg/app.js:142
onClick                              web/src/main.ts:57

Stepping over moves line by line through the Rust function; stepping out returns to the JavaScript glue and then to your TypeScript handler.

Gotchas

  • Variables show as <optimized out>. The dev profile with opt-level = 1 or higher for your crate inlines and eliminates locals. Use opt-level = 0 for the crate you are debugging while keeping dependencies optimized.
  • Stepping is very slow on the first pause. The extension parses DWARF lazily; a large module can take several seconds to index on first use. Subsequent pauses are fast.
  • Breakpoints work in Chrome but not in Node. An older Node may not support the DWARF protocol extensions. Use Node 20 or newer.
  • Source opens but shows the wrong file version. The module is stale — the browser cached an older build. Disable the cache in the launched browser, or make sure the dev server sends no-cache headers.

Performance note

A module with full DWARF was 4.6 MB against 610 KB stripped — 7.5× larger — and took 380 ms longer to load from the local dev server, almost all of it in download and DWARF indexing rather than compilation. That is irrelevant on localhost and a strong reason never to ship debug builds.

Module size with and without debug information The same Rust crate built three ways. Full DWARF dominates the size of a debug build; the release build with debug sections stripped is a small fraction of it. module size in KB dev build, full DWARF 4,710 KB dev build, DWARF stripped 1,180 KB release, stripped 610 KB

Frequently Asked Questions

Do I need the DWARF extension for JavaScript-only debugging? No. It only matters for stepping into WebAssembly at source level. Without it, VS Code shows Wasm frames as disassembled WAT.

Can I debug a release build? Partially. Build release with debug = true in the profile and skip --strip-debug; line mapping works but many variables are optimized away. It is useful for crashes that only reproduce optimized.

Does this work with Emscripten? Yes — compile with -g, and the same extension and launch configuration apply. Emscripten’s -gseparate-dwarf keeps the shipped module small while still allowing source debugging locally.

What about Firefox? VS Code’s Firefox debugger does not support Wasm DWARF debugging. Use Firefox’s own DevTools, as in debugging Wasm in Firefox DevTools.

← Back to Local Development Server Configurations