· 6 min read ·

What Debugging WASM in Chrome Actually Gives You

Source: eli-bendersky

Eli Bendersky recently wrote about debugging WASM in Chrome DevTools while working on the WASM backend for a Scheme compiler. It’s a useful walkthrough, but it also quietly surfaces something worth unpacking: the debugging experience is genuinely better than most people expect, and also more limited than a native equivalent in ways that matter specifically when you’re working with generated code.

Most developers who touch WASM do so through a toolchain that insulates them from the binary. Rust users work through wasm-pack and wasm-bindgen; C/C++ users go through Emscripten; Go has its own WASM target. In those cases, the toolchain handles source maps or DWARF debug info, and Chrome can often show you the original source file. The hard case, which Bendersky’s post is really about, is when you’re writing or generating WAT (WebAssembly Text format) directly and need to understand what the runtime is doing with it.

What the DevTools Actually Show

When you open the Sources panel in Chrome DevTools and navigate to the wasm section, you get a disassembled WAT view of your binary. This is already more than you might expect. WASM’s binary format is compact and not human-readable, but WAT is a structured s-expression format that maps one-to-one onto the binary encoding. You can set breakpoints directly on WAT instructions, step through them, and inspect the value stack.

The value stack is the key mental model. WASM is a stack machine, so at any point during execution you have a set of values on the operand stack. Chrome surfaces these in the scope panel, letting you see what’s on the stack at each instruction. For simple numeric types (i32, i64, f32, f64) this works cleanly. You get the raw value, you can verify your arithmetic is correct, and you can correlate it with what your WAT source says should be there.

Memory inspection is also available. You can view the linear memory as a hex dump, which matters when you’re debugging things like heap layouts, string encoding, or pointer arithmetic. If you’re building a runtime on top of WASM’s linear memory, this is where you actually verify your invariants.

The DWARF Path

For toolchains that emit DWARF debug information embedded in the WASM binary, Chrome can do considerably more. Clang/LLVM targets produce WASM binaries with DWARF sections that describe the original source locations, variable names, and type information. Chrome’s C/C++ DevTools Support extension uses a WASM-based DWARF parser to translate these into source-level views.

With DWARF support active, you can step through C or C++ source files rather than WAT instructions. Local variable names appear instead of stack slot indices. Types are resolved. This is close to what you get with gdb or lldb on a native binary, though there are still rough edges around inlined functions and optimized code.

The important caveat: DWARF quality depends entirely on the toolchain and optimization level. At -O0, you typically get clean source-level debugging. At -O2 or -O3, the compiler has rearranged things enough that the DWARF mappings become approximate. This is not unique to WASM, but it’s worth keeping in mind if you’re chasing a bug that only reproduces in an optimized build.

Where the GC Proposal Complicates Things

Bendersky’s example uses the WASM GC proposal, which adds structured heap types, reference types, and garbage collection to WASM. This is a relatively recent addition; Chrome shipped support for it in late 2023, and it’s now also available in Firefox and Safari.

The GC proposal is what makes it practical to compile languages like Scheme, OCaml, or Kotlin to WASM without having to implement your own garbage collector in linear memory. You use struct and array types defined in the type section, and the runtime handles allocation and collection.

Debugging GC reference types is where things get more interesting. References are opaque handles, not raw pointers you can dereference in the memory view. Chrome does show you GC struct fields in the scope panel when you’re paused on an instruction that has a reference on the stack, but the view is necessarily more abstract than raw memory inspection. You see field values by name if the toolchain emitted type information, or by index if it didn’t.

For generated WAT, as in a compiler backend, you’re typically working without rich type annotations in the debug output. You know you have a (ref $pair) on the stack, but translating that back to “this is the cons cell holding integer 42 and the next pair” requires understanding your own runtime’s layout. Chrome surfaces the structure; you supply the interpretation.

The Local Server Requirement

One practical friction point that’s easy to miss: you cannot open a WASM-loading HTML file directly from the filesystem. The browser will block the WASM fetch due to CORS and same-origin restrictions. You need a local HTTP server.

Bendersky uses static-server, and Python’s built-in http.server module works just as well:

python3 -m http.server 8080

This is a minor friction point but it catches people the first time. It’s also a reason to keep a simple server alias in your shell config if you do any local web development involving WASM.

Comparing the Toolchain Experiences

If you’re used to debugging Rust compiled to WASM via wasm-pack, the experience is different in character. Rust’s toolchain emits DWARF, wasm-pack configures the build to preserve it, and with the DevTools extension you can often step through Rust source. The experience is better than many expect for a web target.

Hand-written or generated WAT debugging is closer to what embedded developers do with microcontrollers: you’re reading disassembly, watching registers (the value stack, in WASM’s case), and reasoning at the instruction level. It’s not hostile; WAT is readable once you’ve spent a few hours with it. But it’s a different mode of thinking than source-level debugging.

The Binaryen toolkit is worth mentioning here. Its wasm-dis tool can produce WAT from a binary, and its optimizer (wasm-opt) can transform WASM in ways that affect debuggability. If you’re generating WASM programmatically, Binaryen gives you a graph-based IR and a set of passes, which is often easier to reason about than raw binary encoding. The trade-off is that optimized Binaryen output can be harder to correlate with your original code generator.

Watgo and the WAT Toolchain

Bendersky uses watgo to compile WAT to WASM. Watgo is a Go-based WAT assembler, which is a nice fit if you’re already in a Go environment. The more established alternative is wat2wasm from the WABT toolkit, which also includes wasm2wat for disassembly, a WASM interpreter, and a validator. WABT is the standard reference implementation toolchain and worth having regardless of your primary language.

For the GC proposal specifically, you need a recent enough version of WABT. The GC types were added to WABT in the 1.0.30 range; older versions will reject WAT files that use struct, array, or ref types.

The Honest Assessment

Chrome’s WASM debugger is genuinely useful, and it has improved substantially since WASM first shipped. For most web-facing use cases, combined with a toolchain that emits proper debug info, it’s sufficient. For compiler backends and runtime implementors working at the WAT level, it gives you exactly what it should: a window into the instruction stream and the execution state, without pretending to be something it isn’t.

The debugging story for WASM is not yet at parity with native, and it probably cannot be for the GC reference types without deeper runtime cooperation. But for the class of bugs that show up in a compiler backend, such as wrong operand ordering, incorrect branch targets, or malformed GC struct construction, the DevTools view gives you enough to make progress without resorting to printf-style logging into the browser console.

That’s a meaningful place to be for a relatively young target architecture.

Was this interesting?