Chrome DevTools as a WASM Debugger
While building the WASM backend for a Scheme compiler, I hit several situations that called for real debugging of generated WASM code. Chrome's DevTools turned out to have a full-featured WASM debugger built in, which made those investigations far less painful than expected.
Everything shown here is available in the gc-print-scheme-pairs sample from the wasm-wat-samples project. That sample builds Scheme-like s-expressions in WASM using garbage-collected references, then prints them recursively. It handles nested pairs of integers, booleans, and symbols.
Getting the sample running
The sample's WAT source needs to be compiled to WASM first, for instance with watgo:
$ cd gc-print-scheme-pairs $ watgo parse gc-print-scheme-pairs.wat -o gc-print-scheme-pairs.wasm
The accompanying browser-loader.html file expects to load gc-print-scheme-pairs.wasm. Because it loads WASM, the page must be served over HTTP rather than opened directly from disk. Any static file server works, for example Python's built-in http.server:
$ static-server 2026/04/10 08:55:20.244096 Serving directory "." on http://127.0.0.1:8080 ...
Follow the printed URL and pick browser-loader.html to open the sample.
Stepping through WASM code
Once the page is up, open DevTools and go to the Sources panel's Page view. Under wasm there will be an entry showing the module's decompiled WAT code. Keep in mind this is disassembled from the binary, so some syntactic sugar from the original WAT, like folded instructions, is lost:
Click the address column next to any instruction to set a breakpoint, then reload the page. The debugger re-runs the program and pauses at that spot:
From there you can step over or into instructions, inspect locals, and view the call stack — a proper debugging experience.
Pinning down unexpected exceptions
The most valuable feature for compiler development is debugging unexpected exceptions, particularly those from instructions like ref.cast. The DevTools pane has checkboxes for pausing on exceptions; when enabled, the debugger stops automatically at the throwing instruction and shows the context.
To demonstrate, add an incorrect cast at the start of the $emit_value function, which normally uses a series of ref.test checks before casting:
(call $emit_bool (ref.cast (ref $Bool) (local.get $v)))
Clearly, assuming $v is a boolean reference without testing it first is wrong, but that's the point of the demonstration.
Recompile with watgo and reload the page — no breakpoints needed. The debugger halts at the instruction that raised the exception:
The Scope pane shows the actual type of $v as (ref $Pair), making the problem immediately obvious. For generated WASM code, especially when emitting non-trivial logic with gc types, this kind of direct feedback is enormously useful.
Debugger versus printf in WASM
There's an ongoing debate between debugger-first and printf-style debugging. While printf debugging is often the pragmatic choice, it doesn't hold up well in WASM for two reasons.
First, WASM's printing facilities are limited. It's possible to import print functions from the host — the sample does exactly that — but they're inflexible, and string handling in WASM is generally painful. That's even worse with gc types since these references are opaque to the host; printing gc values requires building substantial scaffolding.
Second, exception debugging is far easier with a debugger that stops at the exact failure site. A ref.cast exception could originate anywhere in a large program, especially one emitted by a compiler. The debugger takes you right to the offending instruction, sparing you from hunting through thousands of lines of generated code. This mirrors the classic use of gdb for pinpointing segmentation faults in C — for certain failure modes, nothing beats a debugger that shows you precisely where things went wrong.



