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.
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.
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!onwasm32-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.
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.
Related
- Logging from Wasm without slowing it down — production logging.
- Logging from Rust Wasm to the browser console — Rust logging crates.
- Debugging Emscripten builds with assertions — Emscripten checks.
- Inspecting Wasm memory in Chrome DevTools — memory views.
← Back to Debugging & Profiling Wasm Modules