Using printf Debugging in Wasm

This page answers one task: you want to see what a WebAssembly module is doing right now — print a value, confirm a branch ran, dump a structure — without setting up a debugger. Print statements are the fastest way, but where their output goes depends on the language, the toolchain and the target. You want each combination to print somewhere you can see it.

Prerequisites

  • [ ] A module built from C/C++ (Emscripten or wasi-sdk), Rust or Go.
  • [ ] A browser console or a terminal running a WASI runtime.
  • [ ] The ability to rebuild quickly.

Where standard output goes

WebAssembly has no built-in standard output; “printing” means calling an import that the host connects to something visible. Toolchains wire this up differently. Emscripten’s runtime implements stdout and stderr and routes them to console.log and console.error (configurable through Module.print and Module.printErr). WASI targets write to file descriptors 1 and 2 through fd_write, which runtimes like Wasmtime and Node’s WASI connect to the terminal, and browser WASI shims connect to the console. Rust’s wasm32-unknown-unknown target has no standard output at all — println! compiles but writes nowhere — so you use a console-logging crate instead. Go’s GOOS=js target routes fmt.Println to the console through wasm_exec.js; TinyGo similarly.

Where print statements end up Emscripten printf goes to console.log via Module.print. WASI targets write to fd 1 and 2, shown in the terminal by runtimes or the console by browser shims. Rust on wasm32-unknown-unknown has no stdout, so println prints nothing unless a console crate is used. Go with GOOS=js prints to the console through wasm_exec.js. language / target print call appears in C/C++ with Emscripten printf, std::cout console.log (line-buffered) C/C++ or Rust on WASI printf, println! terminal / shim console Rust on wasm32-unknown-unknown println! nowhere — use web_sys::console Go (GOOS=js) / TinyGo fmt.Println console via wasm_exec.js

Step 1 — C and C++ with Emscripten

printf and std::cout work out of the box. Emscripten’s stdout is line-buffered: output appears when a newline is written or the buffer is flushed, so end messages with \n or call fflush(stdout). To send output elsewhere — an on-page panel, a test harness — override the handlers:

const Module = await createModule({
  print: (text) => appendToPanel(text),        // stdout lines
  printErr: (text) => appendToPanel(`ERR ${text}`),
});

For a quick value inspection without formatting code, emscripten_log(EM_LOG_CONSOLE, "%d", x) or EM_ASM({ console.log($0); }, x) prints directly.

Step 2 — Rust in the browser

On wasm32-unknown-unknown, use web_sys::console or a macro wrapping it:

macro_rules! dbg_log {
    ($($t:tt)*) => (web_sys::console::log_1(&format!($($t)*).into()))
}

dbg_log!("decoded {} pixels, first = {:?}", pixels.len(), &pixels[..4]);

Enable the console feature of web-sys. For panics, install console_error_panic_hook so panic messages appear in the console instead of an opaque unreachable trap. On wasm32-wasip1, println! and eprintln! work and appear in the runtime’s terminal.

A print statement from Rust reaching the console The Rust code formats a message with format, converts it into a JavaScript string through wasm-bindgen glue, and calls web-sys console log, which the browser shows in the DevTools console. On WASI targets the same text would instead go through fd_write to the runtime's stdout. format!(...) string in linear memory into JsValue glue decodes UTF-8 console log import web-sys call DevTools console visible output WASI alternative fd_write to stdout

Step 3 — Go and TinyGo

With GOOS=js GOARCH=wasm, fmt.Println writes through the wasm_exec.js runtime to the console; output is buffered until a newline. With GOOS=wasip1, output goes to the WASI runtime’s stdout. TinyGo behaves similarly for its targets. Use println (built-in) for minimal-overhead output in TinyGo where fmt would add size.

Step 4 — print from workers and threads

Output from workers appears in the console with the worker’s context; filter by context in DevTools if output is noisy. Emscripten pthreads proxy stdout from worker threads to the main thread by default, which keeps output ordered but can deadlock if the main thread is blocked; print sparingly from threads, or use emscripten_log, which writes directly from the calling thread. Prefix messages with a thread or worker ID so interleaved output is readable.

Step 5 — dump structures and memory

For structures, Rust’s {:#?} gives readable multi-line output; C needs a small print function per struct. For raw memory, print a hex dump of a region, or — often easier — print the pointer and length from Wasm and inspect memory.buffer from the console with typed arrays. Printing pointers lets you correlate with memory views in Chrome’s Memory Inspector.

Removing debug output before release

Debug prints left in release builds cost time and size, and may leak data. Guard them with a debug feature or cfg(debug_assertions) in Rust, #ifndef NDEBUG in C, or build tags in Go, so they disappear from release builds entirely. A CI check that searches release builds’ data sections for known debug strings catches leftovers.

Printing without pulling in formatting code

Formatting machinery is large: printf in C drags in the full formatter, and Rust’s format! adds tens of kilobytes the first time it is used. For quick debugging that does not matter, but in size-sensitive builds or tiny modules it can change what you are debugging — a module that suddenly grows may compile and optimise differently. Two lighter options help. Import a JavaScript function that prints numbers (log_i32(x), log_f64(x)) and call it directly from Wasm; only the import is added. Or write values into a small array in linear memory and print them from JavaScript after the call returns. Both avoid string formatting inside the module entirely, which also makes them usable in no_std Rust and in hand-written WAT, where no formatter exists at all.

Printing from WAT and minimal modules

For modules written in WAT or built without a standard library, define the print function yourself as an import:

(import "env" "log_i32" (func $log_i32 (param i32)))
(func $debug_point (param $x i32)
  (call $log_i32 (local.get $x)))
const { instance } = await WebAssembly.instantiate(bytes, { env: { log_i32: (x) => console.log("x =", x) } });

Printing strings from such modules means passing a pointer and length and decoding the bytes in JavaScript with TextDecoder — a few lines that are worth keeping in a debug helper module.

Conditional and one-shot prints

Printing on every iteration buries the interesting line among thousands. Print conditionally — only when a value is out of range, only for the 1,000th call, only for one specific input ID — and print once per unique condition by tracking which messages were already shown. A small debug_once! macro that keeps a set of printed message keys is often more useful than a debugger for bugs that happen after many iterations, because it shows exactly the first occurrence.

When to stop printing and start debugging

Print debugging is fastest for “is this code reached?” and “what value does this have here?”. When you find yourself adding prints to every line of a function, or rebuilding repeatedly to narrow down a crash, switch to a debugger with DWARF support in Chrome, or record a trace and replay it under a native debugger — the time spent setting that up is repaid quickly for anything non-trivial.

Keeping a debug helper module

Teams that debug Wasm often benefit from a tiny shared helper: a JavaScript module providing log_i32, log_f64, log_str(ptr, len) and hexdump(ptr, len) imports, plus matching Rust and C declarations. Wiring it into debug builds by default means anyone can add a print in seconds, with consistent prefixes and formatting, and removing it from release builds is a single build flag.

Expected output

The C++ module’s printf lines appear in the console after each newline; the Rust module prints formatted values through a dbg_log! macro and panic messages via the panic hook; a WASI build prints to the terminal with println!; worker output is prefixed with worker IDs; and release builds contain no debug prints.

Gotchas

  • Expecting println! on wasm32-unknown-unknown. It prints nothing. Use the console.
  • Missing newlines with Emscripten. Output stays buffered. End lines or flush.
  • Printing from threads while the main thread blocks. Proxied output can deadlock. Use direct logging.
  • Leaving prints in release. Size, speed and leaks. Compile them out.
  • Printing huge structures in loops. The console slows everything. Print summaries.
  • Printing every iteration. The interesting line drowns. Print conditionally or once per condition.

Performance note

A formatted print per iteration in a tight loop slowed a Rust Wasm function by about 30× with DevTools open; printing once per thousand iterations made the overhead negligible — fine for debugging, but a reminder to remove prints from hot paths.

Slowdown from printing inside a hot loop Relative run time of a Rust Wasm loop with a console print every iteration, a print every thousand iterations, and no printing, with DevTools open. relative run time print every iteration 30 × print every 1, 000 iterations 1.1 × no printing 1 ×

Frequently Asked Questions

Why does my Emscripten output appear late? It is line-buffered; add \n or fflush(stdout).

Can I print from Wasm in Node? Yes — Emscripten and WASI output goes to the terminal; Rust browser builds need console calls, which Node also supports.

Does console.log from Wasm show source locations? It shows the glue’s location; include your own context in the message.

Is there a dbg! equivalent? Rust’s dbg! prints to stderr, which goes nowhere on wasm32-unknown-unknown; use a console macro.

How can I print from a module without a formatter? Import small functions that print numbers, or write values into memory and print them from JavaScript after the call.

When should I switch from prints to a debugger? When prints spread across every line or rebuilds pile up; DWARF debugging or trace replay is faster then.

How do I print a string from a WAT module? Pass a pointer and length to an imported function and decode the bytes from memory.buffer with TextDecoder in JavaScript.

Can I print only the first time something happens? Yes — keep a set of printed message keys and skip repeats, which shows exactly the first occurrence.

← Back to Debugging & Profiling Wasm Modules