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 buildwithout--release, oremcc -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.
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.
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 withopt-level = 1or higher for your crate inlines and eliminates locals. Useopt-level = 0for 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.
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.
Related
- Using the C/C++ DevTools Support extension — the same capability inside Chrome DevTools.
- Reading Wasm stack traces — when a debugger is not attached.
- Handling panics in Rust Wasm — breaking on the panic before the trap.
- Hot reloading a Rust Wasm crate during development — a dev loop that keeps debug info intact.
← Back to Local Development Server Configurations