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

Rust closures passed to JavaScript with Closure::new, Closure::wrap, and Closure::once also catch panics when built with panic=unwind. When a panic occurs inside a closure invoked from JavaScript, the panic is caught and thrown as a PanicError exception.

Like exported functions, catching panics in closures requires the closure to satisfy the UnwindSafe trait.

#![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!"
}

For closures that should not catch panics (and abort the program instead), use the *_aborting variants: new_aborting, wrap_aborting, once_aborting, and once_into_js_aborting. These do not require UnwindSafe.

See Passing Rust Closures to JavaScript for more details on closure panic handling 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

All function arguments must satisfy Rust's UnwindSafe trait. This is automatically handled by wrapping arguments in AssertUnwindSafe, but be aware that this assumes your code handles potential inconsistent state after a panic.

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.

See Also