guide/src/reference/catch-unwind.md
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.
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-Zbuild-std is only available on nightlystd feature - std support is required to use
std::panic::catch_unwind. to catch panics.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
When a synchronous exported function panics, the panic is caught and a
PanicError is thrown to JavaScript:
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"
}
For async functions, panics cause the returned promise to be rejected with a
PanicError:
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"
}
When built with panic=unwind, all closure types catch panics by default and
convert them to JavaScript PanicError exceptions.
&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.
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 constructorsAll 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 UnwindSafeuse 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.
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".
This feature requires a nightly Rust compiler and will not work on stable Rust.
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:
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); });
Functions with &mut [T] slice arguments cannot be used because mutable slices
are not UnwindSafe. Consider using owned types like Vec<T> instead.
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.
catch attribute - For catching
JavaScript exceptions in RustResult<T, E> type - For explicit error handling between
Rust and JavaScript