Debugging Wasm with DWARF and Source Maps
This guide shows how to make Chrome DevTools stop at a breakpoint inside your original Rust or C++
file — with real variable names and types — by embedding DWARF debug information in the .wasm
binary and loading the C/C++ DevTools extension.
Prerequisites
- [ ] Chrome 119+ with DevTools → Settings → Experiments → WebAssembly Debugging: Enable DWARF support enabled
- [ ] The C/C++ DevTools Support (DWARF) extension installed
- [ ] Rust toolchain with
wasm-pack, or Emscripten (emcc) for C/C++ - [ ] A local dev server serving
.wasmasapplication/wasm - [ ]
wabt(wasm-objdump) for verifying the debug sections
What DWARF and source maps actually carry
A source map is a separate .wasm.map JSON file that maps byte offsets in the binary to file,
line, and column in your source. It is small and web-native, but it carries no information about
variable types — DevTools can show you which line you are on, not what a local Vec<u8> contains.
DWARF is the richer format: the same debug-info standard native debuggers use, packed into custom
sections (.debug_info, .debug_line, .debug_str) inside the .wasm. With DWARF plus the
C/C++ extension, the Scope pane resolves locals to their real types. The tradeoff is size — DWARF
can multiply binary size several times over.
The reason both formats are needed at all is that the .wasm the engine executes has no inherent
connection to your source. The compiler lowered your Rust or C++ through several intermediate
representations, did register allocation, inlined functions, and emitted a flat stream of opcodes.
The only record of how byte offset 0x2c41 relates to lib.rs:48 is whatever the compiler chose to
write down. DWARF is that record in its most complete form: a tree of debugging-information entries
describing every function, scope, variable, and type, plus a line-number program that maps code
offsets to source positions. A source map is the same idea stripped to its essentials — just the
offset-to-position mapping, no type tree — which is why it is an order of magnitude smaller and why it
cannot tell you what is inside a variable. Picking between them is a tradeoff between debugging depth
and download weight, and you can even ship both during development and strip both for production.
Procedure
The end-to-end path has five steps: build with debug info, verify it landed in the binary, turn on the DevTools support, set a breakpoint in source, and inspect typed state. The order matters — every later step silently no-ops if an earlier one was skipped, which is why verification comes second rather than last.
1. Build with debug info embedded
For Rust, a development build retains DWARF and the name section. A release build strips them.
wasm-pack build --dev --target web
For C/C++, ask Emscripten for full DWARF:
emcc app.cpp -g -gdwarf-5 -s WASM=1 -o app.js
If you want a source map instead of (or alongside) embedded DWARF from Emscripten — smaller, but
without typed variables — use -gsource-map, which emits app.wasm.map and a sourceMappingURL
custom section:
emcc app.cpp -gsource-map -s WASM=1 -o app.js
2. Verify the debug sections are present
Do not open DevTools until you have confirmed the binary actually carries debug info. This single check explains most “my breakpoints are grey” reports.
wasm-objdump -h pkg/app_bg.wasm
You want .debug_info, .debug_line, .debug_str, and name in the section list. If they are
absent, the build stripped them — re-check that you used --dev / -g and did not run
wasm-strip.
3. Enable DWARF support and load the extension
In DevTools → Settings → Experiments, tick WebAssembly Debugging: Enable DWARF support. Install
the C/C++ DevTools Support (DWARF) extension from the Chrome Web Store. The extension is the
component that parses .debug_info and resolves variable types; the experiment flag tells DevTools
to consult it. Restart DevTools after both are set.
4. Set a breakpoint in your original source
Reload the page. Open the Sources panel — your original lib.rs or app.cpp now appears in the
file tree under the page’s origin, not as a wasm:// disassembly. Click a line-number gutter to set
a breakpoint. Trigger the code path that hits it.
5. Inspect typed variables
When execution pauses, the Call Stack shows real function names (process_frame, not
wasm-function[12]), and the Scope pane lists locals with resolved types. Hover a value to see
it inline; expand a struct to walk its fields. Step with F10 (over) and F11 (into) exactly
as you would in native source.
A few stepping behaviors differ from a typical JavaScript session and are worth knowing up front.
Stepping into a function that has no debug info (a library compiled without -g, or a runtime
intrinsic) drops you into the raw wasm:// disassembly — step out and set your next breakpoint in
source-bearing code instead. Conditional breakpoints work, but the condition is evaluated by the
extension against DWARF-resolved variables, so it must reference names that exist in the current
scope. And because optimized code can map several source lines to one instruction, stepping in a
-O1 or higher build may appear to “jump around” lines; this is correct behavior reflecting the
compiler’s instruction scheduling, not a bug in the mapping.
// lib.rs — set a breakpoint on the `let mid` line
#[wasm_bindgen]
pub fn binary_search(haystack: &[i32], needle: i32) -> i32 {
let (mut lo, mut hi) = (0usize, haystack.len());
while lo < hi {
let mid = lo + (hi - lo) / 2; // ← breakpoint here; inspect lo, hi, mid
if haystack[mid] < needle { lo = mid + 1; } else { hi = mid; }
}
lo as i32
}
Expected output
In the Sources panel you see lib.rs with a blue breakpoint marker on the let mid line. When the
function runs, execution halts there and the Scope pane shows:
Local
haystack: &[i32] (8 elements)
needle: i32 = 42
lo: usize = 0
hi: usize = 8
mid: usize = 4
Function names in the call stack are symbolic, and wasm-objdump -h confirmed the backing sections:
Custom ".debug_info" (size=0x0002d6df)
Custom ".debug_line" (size=0x00011762)
Custom "name" (size=0x00000cc6)
Gotchas
- Release builds strip and inline.
wasm-pack build --releaseremovesDWARFand thename section, and-O2inlines code so a source line may have no instruction to break on — the breakpoint shows hollow grey and never fires. Debug against--dev/-g -O0. - The extension is mandatory for typed variables. Without C/C++ DevTools Support (DWARF)
installed and the experiment enabled, you get a
wasm://disassembly instead of your.rsfile, and locals show as rawi32. Both pieces are required. - The
.debugsections are large. EmbeddedDWARFcommonly makes the binary 5–10× bigger. Error message you will not see — there is no warning; you just notice a 700 KB.wasmwhere the release build is 90 KB. Strip before shipping withwasm-strip app.wasm. - Wrong MIME type drops the source map. If the server returns
text/plainfor the.wasmor.wasm.map, DevTools may not attach thesource mapandinstantiateStreamingthrowsTypeError: Incorrect response MIME type. Serveapplication/wasmandapplication/json.
Performance note
Embedded DWARF is download weight, not runtime weight — the engine ignores .debug_* custom
sections during compilation and execution, so a debug build runs no slower because of the debug
info itself. The slowness of a --dev build comes from skipping optimization (-O0), not from the
DWARF. If you need release-level speed while debugging, build with -O2 -g and accept that inlined
lines may be unbreakable.
There is a second-order cost to be aware of: parsing DWARF is work the C/C++ extension does once
when the module loads, and for a binary with tens of megabytes of debug info that initial parse can
add a noticeable pause before breakpoints become active. This is a one-time, development-only cost —
it never reaches your users because you strip DWARF before shipping — but it explains why a huge
debug build can feel sluggish to attach to. If attaching is painfully slow, narrow the debug info:
compile only the crate you are debugging with full -g and let dependencies build without it, which
keeps the .debug_info tree small enough to parse quickly.
Frequently Asked Questions
Can I use DWARF and source maps at the same time?
You generally pick one. Embedded DWARF supersedes a source map because it carries strictly more
information (types and variables). Emscripten’s -gsource-map and -g are alternative flags; use
-g / -gdwarf-5 when you want full variable inspection.
Why do I see the function name but not its variables?
The name section alone gives readable function names and line-level breakpoints, but variable types
come from .debug_info, which only the C/C++ extension can parse. Confirm .debug_info is present
with wasm-objdump -h and that the extension is installed.
Does this work in Firefox?
Firefox supports source map-based stepping but not the full DWARF typed-variable experience that
the Chrome C/C++ extension provides. For deep variable inspection, use Chrome or Edge.
My breakpoint binds but the variables read as garbage — why?
Almost always an optimization artifact: at -O1 and above the compiler may keep a value in a
register or reuse a stack slot, so the location DWARF records for a variable is only valid for part
of the function. Pausing outside that range shows a stale or unrelated value. Rebuild at -O0 /
--dev to get variables that are live for the whole scope.
Can I debug a .wasm I did not compile?
Only if it was built with debug info. A third-party release binary has no DWARF and usually no
name section, so DevTools can only show you the raw disassembly. You would need the original source
and a debug build from it to get source-level debugging.
Related
- Inspecting Wasm memory in Chrome DevTools — read the bytes behind a pointer once you are paused.
- Wasm binary format deep dive — the custom sections that hold
DWARFand thename section. - Decoding Wasm opcodes for debugging — what to do when no debug info exists.
← Back to Debugging & Profiling Wasm Modules