Passing Rust Closures to Imported JavaScript Functions
The ScopedClosure (with static lifetime alias Closure) and dyn FnMut types are the way to
pass Rust closures to JavaScript. Closure and ScopedClosure are defined in the wasm_bindgen crate and
exported in wasm_bindgen::prelude.
Closures are unwind safe by default: when built with panic=unwind, panics inside
closures are caught and converted to JavaScript PanicError exceptions. See
Catching Panics for details.
Choosing a Closure API
| Use case | Import function signature | Accepts |
|---|---|---|
| Immediate/synchronous callbacks | &dyn Fn / &mut dyn FnMut | &dyn Fn / &mut dyn FnMut only |
| Known-lifetime callbacks | &ScopedClosure<'lifetime, C> | &ScopedClosure<'a>, &ScopedClosure<'static> |
| Indeterminate lifetime | ScopedClosure<'static, C> | ScopedClosure<'static> only |
Constructor Patterns
| Type | Constructor | Aborting Constructor | Assert Unwind Safe |
|---|---|---|---|
&dyn Fn / &mut dyn FnMut | &dyn Fn (Fn) / &mut dyn FnMut (FnMut) | None | None |
&ScopedClosure<'a, C> | Closure::borrow (Fn) / borrow_mut (FnMut) | borrow_aborting / borrow_mut_aborting | borrow_assert_unwind_safe / borrow_mut_assert_unwind_safe |
ScopedClosure<'static, C> | Closure::own (Closure::new) | own_aborting | own_assert_unwind_safe |
ScopedClosure<'static, C> (one-shot) | Closure::once | Closure::once_aborting | once_assert_unwind_safe |
Closure<C> is a backwards compatible alias for ScopedClosure<'static, C>, while providing constructors for arbitrary lifetimes.
The default constructors require UnwindSafe when building with panic=unwind, and catch panics, converting them to JavaScript PanicError exceptions.
The _aborting variants do NOT require UnwindSafe and do NOT catch panics—if the closure panics, the process will abort.
See Catching Panics for details.
The _assert_unwind_safe variants catch panics but don’t require MaybeUnwindSafe, enabling type inference with inline closures while still catching panics. Use these when you need inference and are confident the closure is unwind-safe.
Alternatively, you can wrap your closure with std::panic::AssertUnwindSafe and use the regular constructors (new, new_mut, own, borrow, borrow_mut). This is useful when you want to keep using the coercion-based constructors:
#![allow(unused)]
fn main() {
use std::panic::AssertUnwindSafe;
use wasm_bindgen::prelude::*;
let data = Rc::new(RefCell::new(0));
let closure = Closure::own(AssertUnwindSafe(move || {
*data.borrow_mut() += 1;
}));
}
This constructor flexibility allows API consumers to decide on unwind safety behavior at the call site, rather than having it fixed by the function signature. A single function accepting ScopedClosure <dyn FnMut(u32)> can be called with closures created via with or without unwind safety, with varying or static lifetimes, and with optional once semantics.
&dyn Fn and &mut dyn FnMut
The #[wasm_bindgen] attribute supports passing closures as &dyn Fn or
&mut dyn FnMut trait object references directly.
When using this pattern, unwind safety is assumed. It is therefore up to the user to ensure that they are using this pattern with unwind safe function usage.
#![allow(unused)]
fn main() {
#[wasm_bindgen]
extern "C" {
fn takes_closure(f: &dyn Fn());
fn takes_mut_closure(f: &mut dyn FnMut());
}
}
For longer-lived closures, or for closures that support checking unwind safety, use ScopedClosure instead.
Known-Lifetime Callbacks with ScopedClosure
For longer lived closures, use ScopedClosure, which operates in two separate modes
depending on whether it uses a static lifetime or a known lifetime.
When typing a JS function from Rust taking a closure argument there are two modes of operation:
-
&ScopedClosure<'a, T>(pass by ref): The closure is borrowed out to JS, while retaining ownership in Rust. There is no JS GC finalizer integration. It is disposed by Rust when dropped. Works with any lifetime. -
ScopedClosure<'static, T>by value (pass by value): The closure is passed to JS ownership and integrated with JS GC finalizers. It is disposed entirely by JS. Only works with'staticclosures.
Note that ScopedClosure<'static, T> can also be passed by reference (&ScopedClosure<'static, T>)
if you want Rust to retain ownership of a static closure.
Non-Static Lifetimes (Pass by Reference Only)
For creating a ScopedClosure<'a, T> from a non-static lifetime, use Closure::borrow (for immutable Fn)
or Closure::borrow_mut (for mutable FnMut) when JavaScript may store the callback temporarily but
you control when it becomes invalid. The closure is invalidated when the ScopedClosure is dropped.
Non-static closures can only be passed by reference since the underlying closure data may live on the stack.
These are unwind safe. For non-unwind-safe closures, use Closure::borrow_aborting and
Closure::borrow_mut_aborting (aborts on panic), or use Closure::borrow_assert_unwind_safe
to assert unwind safety while still catching panics.
#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn register_callback(cb: &ScopedClosure<dyn FnMut(u32)>);
fn trigger_callbacks();
}
let mut result = 0;
{
let mut func = |value: u32| {
result += value;
};
let closure = Closure::borrow_mut(&mut func);
register_callback(&closure);
trigger_callbacks(); // Calls our closure
// closure dropped here, invalidating the JS reference
}
}
The validity of the JavaScript function is tied to the lifetime of the ScopedClosure
in Rust. Once a ScopedClosure is dropped, it will deallocate its internal memory
and invalidate the corresponding JavaScript function so that any further
attempts to invoke it raise an exception.
If JavaScript calls the closure after the ScopedClosure is dropped, it will
throw: “closure invoked recursively or after being dropped”.
Static Lifetimes with Closure<T> = ScopedClosure<'static, T>
For 'static closures, use Closure::own() when JavaScript needs to retain the
closure for an indeterminate period, such as for event listeners, timers, or
callbacks that outlive the current function call.
Note: It is recommended in function signatures to use
ScopedClosureand notClosure, as it can accept both short-lived and static closuresClosure. This is becauseClosure<T>is an alias forScopedClosure<'static, T>. In a future release,Closurewill be directly aliased toScopedClosureinstead._
#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
fn clearInterval(token: f64);
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub struct Interval {
closure: Closure<dyn FnMut()>,
token: f64,
}
impl Interval {
pub fn new<F: 'static>(millis: u32, f: F) -> Interval
where
F: FnMut()
{
// Construct a new closure.
let closure = Closure::new(f);
// Pass the closure to JS, to run every n milliseconds.
let token = setInterval(&closure, millis);
Interval { closure, token }
}
}
// When the Interval is destroyed, clear its `setInterval` timer.
impl Drop for Interval {
fn drop(&mut self) {
clearInterval(self.token);
}
}
// Keep logging "hello" every second until the resulting `Interval` is dropped.
#[wasm_bindgen]
pub fn hello() -> Interval {
Interval::new(1_000, || log("hello"))
}
}
Transferring Ownership to JavaScript
You can pass a ScopedClosure by value to transfer ownership to JavaScript. This is
useful for one-shot callbacks where you don’t need to retain a handle in Rust:
#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn set_one_shot_callback(cb: ScopedClosure<dyn FnMut()>);
}
let cb = Closure::own(|| {
// This closure must be 'static
});
set_one_shot_callback(cb); // Ownership transferred to JS GC, no need to store or forget
}
Note that only 'static closures (ScopedClosure<'static, T>) can be passed
by value. Borrowed closures must be passed by reference.
One-Shot Static Closures with ScopedClosure<'static, T>::once
Use Closure::once (or Closure::once alias) for closures that should only be
called once, such as Promise handlers. This allows using FnOnce closures that
consume captured values.
#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
// Create a closure that consumes a String
let message = String::from("Hello!");
let closure: Closure<dyn FnMut()> = Closure::once(move || {
// message is moved and consumed here
web_sys::console::log_1(&message.into());
});
}
If you don’t need to cancel the closure early, use Closure::once_into_js to
convert directly to a JsValue. Note that if the JavaScript function is never
called, the FnOnce and everything it closes over will leak.
#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
let callback = Closure::once_into_js(move || {
// This runs when called from JS
});
// callback is a JsValue containing a JS function
}
Panic Handling
When built with panic=unwind, all ScopedClosure and dyn Fn variants
catch panics and convert them to JavaScript PanicError exceptions. This requires
the closure to satisfy Rust’s UnwindSafe trait.
For more information on enabling panic catching, see Catching Panics.
UnwindSafe Requirement
The closure constructors for ScopedClosure all require that
closures be UnwindSafe. They act as marker traits that indicates a type is safe to
use across panic boundaries.
Common “not unwind safe” compiler errors are caused by capturing types with interior mutability:
Rc<Cell<_>>,Rc<RefCell<_>>- Other interior mutability types
The compiler error will indicate which captured type is problematic.
Fix 1: Use aborting variants
If you don’t need panic catching, use the *_aborting variants (own_aborting,
once_aborting, Closure::borrow_aborting, Closure::borrow_mut_aborting) which do not require UnwindSafe:
#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
let data = Rc::new(RefCell::new(0));
// No UnwindSafe requirement — aborts on panic instead of catching
let closure = Closure::own_aborting(move || {
*data.borrow_mut() += 1;
});
}
Fix 1b: Use assert_unwind_safe variants
For ScopedClosure, you can use Closure::own_assert_unwind_safe directly:
#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
let data = Rc::new(RefCell::new(0));
// No UnwindSafe requirement — catches panics, caller asserts safety
let closure: Closure<dyn FnMut()> = Closure::own_assert_unwind_safe(move || {
*data.borrow_mut() += 1;
});
}
Or use Closure::wrap_assert_unwind_safe with a boxed closure:
#![allow(unused)]
fn main() {
let data = Rc::new(RefCell::new(0));
let closure: Closure<dyn FnMut()> = Closure::wrap_assert_unwind_safe(Box::new(move || {
*data.borrow_mut() += 1;
}));
}
Fix 2: Assert unwind safety
If you need panic catching and are confident your closure is safe to use across
panic boundaries, you can use AssertUnwindSafe:
#![allow(unused)]
fn main() {
use std::panic::AssertUnwindSafe;
use wasm_bindgen::prelude::*;
let closure = Closure::new(AssertUnwindSafe(move || {
// you're asserting this is safe across panics
}));
}
Upcasting ScopedClosure
ScopedClosure supports full JS type variance within the erasable generic type system via upcast()
and upcast_into(). This is possible since it eagerly moves the Rust function to a JS function on
construction, in contrast to dyn FnMut whose Repr is its Rust fat pointer and not its
JsValue representation.
This enables covariance and contravariance on argument and return types:
#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use js_sys::Number;
// Return type covariance: i32 -> Number -> JsValue
let closure: Closure<dyn Fn() -> i32> = Closure::own(|| 42i32);
let _wider: &Closure<dyn Fn() -> JsValue> = closure.upcast_ref();
// Argument contravariance: JsValue -> Number -> i32
let closure: Closure<dyn Fn(JsValue)> = Closure::own(|_: JsValue| {});
let _narrower: &Closure<dyn Fn(i32)> = closure.upcast_ref();
}
This works because the JavaScript function doesn’t care about Rust’s type distinctions—the
conversion between i32, Number, and JsValue happens at the JS-Wasm boundary,
not inside the closure itself.
Converting Closures to Typed Functions
The js_sys::Function type provides methods for type-safe conversion from Closure/ScopedClosure to typed Function with comprehensive covariance support:
| Method | Input | Output | Use Case |
|---|---|---|---|
from_closure | ScopedClosure<'static, C> | Function<F> | Owned closures (transfers ownership to JS) |
closure_ref | &ScopedClosure<C> | &Function<F> | Borrowed closures |
#![allow(unused)]
fn main() {
use js_sys::{Function, Number, JsString};
use wasm_bindgen::prelude::*;
// Owned static conversion - transfers ownership to JS
let closure: Closure<dyn FnMut() -> u32> = Closure::new(|| 42);
let func: Function<fn() -> Number> = Function::from_closure(closure);
// Borrowed ScopedClosure conversion
let mut val: u32 = 5;
let mut func = || { val += 1; val };
let closure = ScopedClosure::borrow_mut(&mut func);
let func_ref: &Function<fn() -> Number> = Function::closure_ref(&closure);
}
Passing Closures to Typed Callback APIs
Many JavaScript APIs like DOMTokenList.forEach or Array.forEach accept typed callback
functions. You can pass Rust closures to these APIs using Function::closure_ref with
ScopedClosure::borrow_mut:
#![allow(unused)]
fn main() {
use js_sys::{Array, Function, JsString, Number, Undefined};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
// Simulates APIs like DOMTokenList.forEach that take typed callbacks
fn invoke_for_each_callback(
callback: &Function<fn(JsString, Number) -> Undefined>,
items: &Array<JsString>,
);
}
let items: Array<JsString> = Array::new_typed();
items.push(&JsString::from("apple"));
items.push(&JsString::from("banana"));
let mut results = Vec::new();
// Rust closure borrowing local data
let mut func = |value: JsString, index: Number| {
results.push((value.as_string().unwrap(), index.value_of() as u32));
};
// ScopedClosure borrows the Rust closure, Function::closure_ref provides
// a typed &Function reference for the JS callback parameter
invoke_for_each_callback(
Function::closure_ref(&ScopedClosure::borrow_mut(&mut func)),
&items
);
// After the call, results contains: [("apple", 0), ("banana", 1)]
}
This pattern works with any API expecting a typed &Function<fn(...) -> ...> callback,
such as the generated web-sys bindings for DOMTokenList::for_each, NodeList::for_each,
and similar iteration methods.
Future improvement: If
js-sysis migrated intowasm-bindgencore in a future release, it will be possible to implementDeref<Target = Function<F>>forScopedClosure, enabling automatic dereferencing and Rust-to-JS type conversions. This would simplify the above pattern to justinvoke_for_each_callback(&ScopedClosure::borrow_mut(&mut func), &items).