Debugging Code on a WebAssembly Runtime

Shopify Functions lets developers customize back-end business logic by running server-side WebAssembly. The environment offers advantages like low cold-start latency and strong security, but it also brings a distinct debugging problem: step debuggers don’t work as smoothly against WebAssembly as they do against native code. Step debugging—pausing at breakpoints, walking through lines, inspecting in-scope variables—is a workflow developers often take for granted until it no longer behaves the way it does for compiled native binaries.

The underlying issue is not unique to Shopify Functions. Any server-side WebAssembly deployment faces it. There are four viable paths:

  • Compile to native code and debug with a language-native debugger (LLDB, GDB, etc.).
  • Run the WebAssembly binary in Wasmtime under LLDB.
  • Use a WebAssembly-specific debugger.
  • Debug inside a browser via developer tools.

None of these is ideal in every situation, so the right choice depends on what exactly is broken.

Debug Native Code When Possible

Compiling to a native architecture and using a mature debugger like LLDB or GDB generally produces the best debugging experience. Native debuggers render variable values with high fidelity, and most developers already know how to use them. For interpreted languages such as JavaScript or Ruby, native debugging is the only practical route, because the code runs inside a WebAssembly-compiled interpreter. There’s no direct mapping from source to WebAssembly instructions that a debugger can follow.

One initial hurdle: WebAssembly modules typically import external functions, so calls to those functions may need stubs or replacements to link natively. Preprocessor directives or build configurations can swap implementations depending on the target architecture. This is unnecessary for WASI calls, which are already supported natively.

In the typical case, external input travels through WASI’s fd_read. When compiling natively, that call might be replaced with a reader over a hard-coded string—a pattern that can be adapted for any imported function. To debug Rust code natively, run cargo build without --target, then execute rust-lldb target/debug/your_app_name. The CodeLLDB extension brings the same control into VS Code.

Native debugging has limits. It is not an option for languages that only target WebAssembly, like AssemblyScript or Grain, and it is useless when the bug only appears inside the WebAssembly runtime.

Stepping Through WebAssembly in Wasmtime with LLDB

When the problem only shows up in a WebAssembly engine, running the .wasm file under Wasmtime and attaching LLDB can help. Wasmtime translates debug symbols as it compiles to native code, which lets LLDB map machine instructions back to source lines. It is also the path to take when the code relies on WebAssembly features that browser-based tools or WebAssembly-specific debuggers don’t support.

The trade-offs show up in the details. Variable rendering is less polished: strings that are trivial to inspect natively require extra effort in this setup, and Rust variables may not appear at all. Native rust-lldb renders Rust vectors and strings well, but rust-lldb generally doesn’t work with Wasmtime’s generated debug symbols. Plain LLDB, meanwhile, mangles Rust data structures.

Wasmtime’s documentation and Shopify’s own function-runner tooling both cover how to get this setup working.

WebAssembly-Native Debuggers

A standalone tool like Wasminspect approaches the problem from the WebAssembly side. It offers direct inspection of linear memory and lets you disassemble actual WebAssembly instructions rather than the native code emitted by the runtime engine. But dedicated tools in this space are young: they lack features most developers assume, including setting breakpoints by source line, and they don’t yet integrate via the debugger adapter protocol into visual editors. Non-numeric variable values also display poorly.

Debugging in the Browser

Browser-based debugging offers two sub-approaches. The first uses Chrome’s beta C/C++ DevTools Support (DWARF) extension, which understands WebAssembly debug information directly. The second converts debug symbols into a source map, then patches the WebAssembly file to reference it. That route requires writing a small HTML and JavaScript harness to instantiate the module, and—if the module relies on WASI—stubbing those calls or supplying a browser-compatible WASI implementation.

The Chrome extension renders C strings correctly but falls over on wide strings. Converting debug symbols to a source map via wasm-dwarf or the equivalent Emscripten tool works, but local variables become invisible in the debugger. Chrome’s memory inspector can still show linear memory at specific addresses when that’s enough information.

Rough edges aside, browser tooling may become the most approachable option in the long run, since the debugging setup is lighter than CLIs and server processes.

Choosing an Approach

Native debugging remains the most effective option for the widest range of issues today. When the defect only occurs under the WebAssembly engine, Wasmtime with LLDB or browser tooling are the pragmatic alternatives. The fastest-maturing parts of the ecosystem will likely shift these recommendations, but for now the choice comes down to whether the bug reproduces natively or only inside WebAssembly.