Skip to Content
Course content

215: Debugging WebAssembly Modules

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent more hours than I care to admit staring at a browser console that simply says RuntimeError: unreachable. If you've ever felt the frustration of your Rust code panicking inside WebAssembly and getting absolutely zero information about where it happened, you're not alone. By default, WASM is a black box. When it panics, it doesn't have a standard way to print a backtrace to the JS console; it just traps and dies.

Take a look at this snippet. I'm writing a simple function to process a slice of data passed from JavaScript. It looks straightforward, but there's a logic error here:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn process_sensor_data(data: &[f32]) -> f32 {
    // I'm assuming the data always has at least two elements
    // to calculate a delta, but I'm not checking.
    let delta = data[1] - data[0]; 
    delta * 1.5
}

If the JavaScript side accidentally passes an empty array or a single-element array, this code will panic. In a native Rust app, you'd get a beautiful "index out of bounds" message with a file and line number. In the browser? You get RuntimeError: unreachable. That's it. No line number, no file, no clue.

The Dreaded 'Unreachable' Execution Error

The reason this happens is that Rust's panic machinery is designed for OS-level environments. In WASM, there's no "stderr" to print to. The panic_abort strategy (which is common in WASM) simply tells the WASM virtual machine to stop executing immediately. This is great for binary size, but it's a nightmare for debugging.

To fix this, we need to redirect Rust's panic messages into the browser's console.error. This is where the console_error_panic_hook crate comes in. It's essentially a bridge that captures the panic information and formats it as a string that JavaScript can actually display.

Getting a Real Stack Trace with console_error_panic_hook

First, you'll need to add the dependency to your Cargo.toml. Once that's done, you have to "install" the hook at the very beginning of your WASM module's execution. If you don't call it before the panic happens, you're right back to square one.

use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    // This is the magic line. It tells Rust: 
    // "If you panic, send the message to console.error"
    console_error_panic_hook::set_once();
    Ok(())
}

#[wasm_bindgen]
pub fn process_sensor_data(data: &[f32]) -> f32 {
    let delta = data[1] - data[0]; 
    delta * 1.5
}

Now, when that index-out-of-bounds error hits, your console will actually say: panicked at 'index out of bounds: the len is 0 but the index is 1'. It's not a full native backtrace, but it's a thousand times better than "unreachable".

Leveraging DWARF and Browser DevTools

Even with the panic hook, you're still missing the exact line number in your .rs file because the browser is executing compiled WASM, not your source code. To bridge this gap, you need debug symbols.

If you are using wasm-pack build, it usually handles a lot of this, but you should ensure you aren't building in --release mode while debugging. Release builds strip out the DWARF info to save space. When you build in debug mode, the .wasm file contains a mapping back to your original source.

In Chrome, you can enable "WebAssembly Debugging" in the DevTools settings. Once enabled, if you have a panic (or a breakpoint), the browser can actually map the WASM instruction back to your Rust line. I highly recommend using a debugger; statement in your JS wrapper or using the web_sys::console::log_1 function to trace data flow before it hits the Rust boundary. Combining console_error_panic_hook for crashes and DWARF symbols for stepping through code is the only way to keep your sanity when your WASM module gets complex.




📋 Practical Task

Fixing the Panic in the Image Contrast Filter

You have been handed a WASM module designed to adjust the contrast of an image. The Rust code expects a flat array of pixels (RGBA), but it's crashing with a RuntimeError: unreachable when processing certain images. Your goal is to diagnose the exact cause of the crash and fix it.

The Setup: You are provided with a Rust function that iterates through a pixel buffer. The bug is that the code assumes the buffer length is always a multiple of 4, but the JavaScript side is occasionally sending a buffer that is truncated.

// Current broken implementation
#[wasm_bindgen]
pub fn apply_contrast(pixels: &mut [u8], factor: f32) {
    for i in (0..pixels.len()).step_by(4) {
        // This will panic if pixels.len() is not a multiple of 4
        // because it accesses i+1, i+2, and i+3 without checking.
        let r = pixels[i] as f32;
        let g = pixels[i+1] as f32;
        let b = pixels[i+2] as f32;
        
        pixels[i] = ((r - 128.0) * factor + 128.0).clamp(0.0, 255.0) as u8;
        pixels[i+1] = ((g - 128.0) * factor + 128.0).clamp(0.0, 255.0) as u8;
        pixels[i+2] = ((b - 128.0) * factor + 128.0).clamp(0.0, 255.0) as u8;
    }
}

Your Task:

  1. Integrate console_error_panic_hook into the module so the browser reveals the "index out of bounds" error instead of the generic "unreachable" message.
  2. Modify the apply_contrast function to safely handle buffers that might not be perfectly aligned to 4 bytes (e.g., by using chunks_exact_mut(4) or adding a length check).
  3. Verify that the module no longer crashes when passed a slice of length 7.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.