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 tounwind(notabort)-Zbuild-std- Required to rebuild the standard library with theunwindpanic strategy- Rust nightly compiler -
-Zbuild-stdis only available on nightly stdfeature -stdsupport is required to usestd::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 theUnwindSafecheck*_aborting— does not catch panics (aborts instead) and does not requireUnwindSafe
#![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:
nameproperty set to"PanicError"messageproperty 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
catchattribute - For catching JavaScript exceptions in RustResult<T, E>type - For explicit error handling between Rust and JavaScript