Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Catching Panics

By default, when a Rust function exported to JavaScript panics, Rust will abort and any allocated resources will be leaked. If you build with with -Cpanic=unwind and the std feature, Rust panics will be caught at the JavaScript boundary and converted into JavaScript exceptions, allowing proper cleanup and error handling.

When enabled, panics in exported Rust functions are caught and thrown as PanicError exceptions in JavaScript. For async functions, the returned promise is rejected with the PanicError.

Requirements

  • panic=unwind - The panic strategy must be set to unwind (not abort)
  • -Zbuild-std - Required to rebuild the standard library with the unwind panic strategy
  • Rust nightly compiler - -Zbuild-std is only available on nightly
  • std feature - std support is required to use std::panic::catch_unwind. to catch panics.

Building

Build your project with the required flags:

RUSTFLAGS="-Cpanic=unwind" cargo +nightly build --target wasm32-unknown-unknown -Zbuild-std=std,panic_unwind

Or set these in .cargo/config.toml:

[unstable]
build-std = ["std", "panic_unwind"]

[build]
target = "wasm32-unknown-unknown"
rustflags = ["-C", "panic=unwind"]

[profile.release]
panic = "unwind"

Then build with:

cargo +nightly build

How It Works

Synchronous Functions

When a synchronous exported function panics, the panic is caught and a PanicError is thrown to JavaScript:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("division by zero");
    }
    a / b
}
}
import { divide } from './my_wasm_module.js';

try {
    divide(10, 0);
} catch (e) {
    console.log(e.name);    // "PanicError"
    console.log(e.message); // "division by zero"
}

Async Functions

For async functions, panics cause the returned promise to be rejected with a PanicError:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub async fn fetch_data(url: String) -> Result<JsValue, JsValue> {
    if url.is_empty() {
        panic!("URL cannot be empty");
    }
    // ... fetch implementation
    Ok(JsValue::NULL)
}
}
import { fetch_data } from './my_wasm_module.js';

try {
    await fetch_data("");
} catch (e) {
    console.log(e.name);    // "PanicError"
    console.log(e.message); // "URL cannot be empty"
}

Closures

When built with panic=unwind, all closure types catch panics by default and convert them to JavaScript PanicError exceptions.

Direct closures (&dyn Fn / &mut dyn FnMut)

Direct closure arguments (&dyn Fn and &mut dyn FnMut) are unwind safe by default. The #[wasm_bindgen] macro auto-injects a MaybeUnwindSafe bound, so the compiler will require callers to wrap non-unwind-safe captured values (e.g. Cell<T>, &mut T) in std::panic::AssertUnwindSafe. See Passing Rust Closures to JavaScript for details and examples.

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    fn forEach(cb: &mut dyn FnMut(u32));
}

forEach(&mut |x| {
    if x == 0 {
        panic!("zero not allowed!");
    }
});
}

ScopedClosure constructors

All default ScopedClosure constructors (new/own, wrap, once, once_into_js, borrow, borrow_mut) catch panics and require UnwindSafe. Each has two alternative suffixed variants:

  • *_assert_unwind_safe — catches panics but skips the UnwindSafe check
  • *_aborting — does not catch panics (aborts instead) and does not require UnwindSafe
#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    fn setCallback(f: &Closure<dyn FnMut()>);
}

let closure = Closure::new(|| {
    panic!("closure panicked!");
});
setCallback(&closure);
}
try {
    triggerCallback();
} catch (e) {
    console.log(e.name);    // "PanicError"
    console.log(e.message); // "closure panicked!"
}

See Passing Rust Closures to JavaScript for more details on closure APIs and the UnwindSafe requirement.

The PanicError Class

When a panic occurs, a PanicError JavaScript exception is created with:

  • name property set to "PanicError"
  • message property containing the panic message (if the panic was created with a string message)

If the panic payload is not a String or &str (e.g., panic_any(42)), the message will be "No panic message available".

Limitations

Nightly Only

This feature requires a nightly Rust compiler and will not work on stable Rust.

UnwindSafe Requirement

Exported function arguments and closure captures must satisfy Rust’s UnwindSafe trait. For &dyn Fn and &mut dyn FnMut import arguments the macro enforces this via an auto-injected MaybeUnwindSafe bound. For captured values that are not UnwindSafe (such as &mut T, Cell<T>, or RefCell<T>), wrap them in std::panic::AssertUnwindSafe before the closure captures them:

#![allow(unused)]
fn main() {
let cell = std::cell::Cell::new(0u32);
let cell_ref = std::panic::AssertUnwindSafe(&cell);
takes_mut_closure(&mut move || { cell_ref.set(cell_ref.get() + 1); });
}

Mutable Slice Arguments

Functions with &mut [T] slice arguments cannot be used because mutable slices are not UnwindSafe. Consider using owned types like Vec<T> instead.

Hard Aborts

Some errors — unreachable instructions, stack overflow, or out-of-memory — are non-recoverable and cannot be caught by catch_unwind. When a hard abort occurs the Wasm instance is permanently poisoned and subsequent export calls will throw "Module terminated".

wasm-bindgen provides abort handlers and a reinit mechanism for responding to these events and optionally recovering. See Handling Aborts for details on set_on_abort, schedule_reinit(), set_on_reinit, and host-initiated termination.

See Also