Working with JavaScript Classes from Rust
This page answers one task: Rust code in a WebAssembly module must create and use instances of JavaScript classes — your application’s own classes, a
library’s classes, or built-in ones not covered by js-sys and web-sys — calling constructors, methods and properties with type checking, and
recognising instances passed in from JavaScript.
Prerequisites
- [ ] A Rust crate using wasm-bindgen.
- [ ] The JavaScript class available as an ES module export, or as a global.
- [ ] Knowledge of the class’s constructor, methods and properties.
How wasm-bindgen imports classes
wasm-bindgen describes JavaScript APIs to Rust with extern "C" blocks annotated with #[wasm_bindgen]. A type declaration inside such a block becomes a
Rust type representing instances of a JavaScript class — an opaque handle, internally a JsValue. Functions in the block annotated with constructor,
method, static_method_of, getter and setter become Rust methods that call the corresponding JavaScript operations through generated glue.
module = "..." tells wasm-bindgen which ES module to import the class from; js_namespace handles globals such as Intl.Collator.
Rust gets static types for the parts of the class it declares; nothing checks at compile time that the declarations match the real class, so
mistakes show up as runtime TypeErrors. Declare only what Rust uses, and keep the declarations next to tests that exercise them.
Step 1 — declare the class
Given a JavaScript class in your application:
// chart.js
export class Chart {
constructor(element, options) { /* ... */ }
addSeries(name, values) { /* ... */ return this.series.length - 1; }
get seriesCount() { return this.series.length; }
set title(t) { this._title = t; }
static fromConfig(config) { return new Chart(document.body, config); }
}
Declare it for Rust:
use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "/js/chart.js")]
extern "C" {
pub type Chart;
#[wasm_bindgen(constructor)]
pub fn new(element: &web_sys::Element, options: &JsValue) -> Chart;
#[wasm_bindgen(method, js_name = addSeries)]
pub fn add_series(this: &Chart, name: &str, values: &[f64]) -> u32;
#[wasm_bindgen(method, getter, js_name = seriesCount)]
pub fn series_count(this: &Chart) -> u32;
#[wasm_bindgen(method, setter)]
pub fn set_title(this: &Chart, title: &str);
#[wasm_bindgen(static_method_of = Chart, js_name = fromConfig)]
pub fn from_config(config: &JsValue) -> Chart;
}
module = "/js/chart.js" is resolved relative to the crate root and copied into the output; for npm packages use the package name
(module = "chart-lib") and let the bundler resolve it.
Step 2 — create and use instances
#[wasm_bindgen]
pub fn draw_report(el: &web_sys::Element, data: &Report) {
let chart = Chart::new(el, &serde_wasm_bindgen::to_value(&data.options).unwrap());
for s in &data.series {
chart.add_series(&s.name, &s.values);
}
chart.set_title(&format!("{} series", chart.series_count()));
}
Each call crosses the boundary; slices such as &[f64] are copied into new Float64Arrays. The Chart value is dropped at the end of the function,
releasing Rust’s reference — the JavaScript object lives on as long as JavaScript references it (here, through the DOM).
Step 3 — check instances with instanceof
When JavaScript passes values into Rust as JsValue, use JsCast to check and convert. dyn_into performs an instanceof check against the imported
class:
use wasm_bindgen::JsCast;
#[wasm_bindgen]
pub fn attach(target: JsValue) -> Result<(), JsValue> {
let chart: Chart = target.dyn_into().map_err(|_| JsValue::from_str("expected a Chart"))?;
chart.set_title("attached");
Ok(())
}
dyn_into fails for objects from another realm (an iframe) or for duck-typed objects that are not real instances; if you need structural checks,
declare #[wasm_bindgen(is_type_of = ...)] with a custom predicate, or use unchecked_into when you trust the caller.
Step 4 — extend behaviour without subclassing
Rust cannot subclass a JavaScript class directly — wasm-bindgen has no way to declare a Rust type that extends an imported class and overrides methods.
Two patterns cover most needs. Composition: wrap the JavaScript object in a Rust struct and add behaviour around it. Callbacks: if the class supports
hooks (event listeners, strategy objects, render callbacks), pass Rust closures as those hooks. When subclassing is truly required — a framework that
only accepts subclasses, such as custom elements — write the subclass in JavaScript and have its methods delegate to Rust exports:
import { render_widget } from "./pkg/widgets.js";
class WasmWidget extends HTMLElement {
connectedCallback() { render_widget(this); }
}
customElements.define("wasm-widget", WasmWidget);
#[wasm_bindgen(extends = ...)] exists for declaring an imported type’s superclass, so Rust can use parent-class methods on it (as web-sys does for
HtmlElement extends Element); it does not create subclasses.
Step 5 — keep declarations correct
Because declarations are unchecked, test them: a wasm-bindgen-test that constructs the class, calls each declared method and reads each property
catches renamed methods and changed signatures. For classes from npm libraries with TypeScript types, compare declarations against the .d.ts when
upgrading the library. Return types deserve particular care: declaring a method as returning u32 when it returns undefined produces 0 silently;
declaring it Option<u32> makes the absence visible.
Properties, statics and globals
Getters and setters map to getter/setter methods; use js_name when JavaScript uses camelCase. Static properties use
#[wasm_bindgen(static_method_of = Chart, getter)]. Global classes — those not exported from a module, such as Intl.NumberFormat or a library loaded via a
script tag — use js_namespace instead of module:
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = Intl, js_name = NumberFormat)]
pub type NumberFormat;
#[wasm_bindgen(constructor, js_namespace = Intl, js_class = "NumberFormat")]
pub fn new(locale: &str, options: &JsValue) -> NumberFormat;
#[wasm_bindgen(method)]
pub fn format(this: &NumberFormat, n: f64) -> String;
}
Performance considerations
Each method call is a boundary crossing with argument conversion. Calling a JavaScript method per element in a large loop — chart.add_point(x, y) a
million times — costs far more than passing arrays once. Design the JavaScript class or a thin adapter to accept bulk data (addSeries(name, values)
rather than addPoint), and cache instances in Rust rather than re-creating them per call.
Errors thrown by JavaScript methods
A JavaScript method can throw, and by default a wasm-bindgen import declared with a plain return type assumes it does not: if it throws, the exception
unwinds through the Wasm frames and surfaces at the outermost JavaScript caller, skipping any Rust cleanup in between and potentially leaving Rust state
half-updated. Declare methods that may throw with catch, which changes the Rust signature to return Result<T, JsValue>:
#[wasm_bindgen(method, catch, js_name = addSeries)]
pub fn try_add_series(this: &Chart, name: &str, values: &[f64]) -> Result<u32, JsValue>;
Rust then handles the error explicitly — retrying, mapping it to a Rust error, or propagating it with ? from an export that itself returns Result.
Use catch for anything that touches user input, the network, storage, or third-party code; for simple getters that cannot fail, the plain form is fine
and slightly cheaper.
Keeping JavaScript objects alive from Rust
A Rust value of an imported class type holds a reference that keeps the JavaScript object alive until the Rust value is dropped. Storing such values in long-lived Rust structures — a global registry of charts, a cache of formatters — keeps the JavaScript objects (and whatever they reference, such as DOM nodes) alive indefinitely. That is often intended, but it is also a common source of leaks after the UI has removed the corresponding element. Pair every long-lived reference with an explicit removal path, and when Rust only needs to refer to an object occasionally, consider keeping it in a JavaScript-side map keyed by an ID and passing the ID to Rust instead.
Expected output
Rust creates Chart instances with Chart::new, adds series with typed slices, sets the title through a setter and reads seriesCount through a getter;
attach rejects non-Chart values with a clear error; a custom element subclass written in JavaScript delegates rendering to Rust; and a test exercises
every declared method so a renamed JavaScript method fails CI.
Gotchas
- Declarations drifting from the class. Runtime
TypeErrors. Test every declared member. dyn_intoacross iframes.instanceoffails across realms. Use custom type checks.- Expecting Rust subclasses. Not supported. Compose, use callbacks, or subclass in JavaScript.
- Wrong return types.
undefinedbecomes0silently. UseOptionwhere values can be missing. - Per-element method calls. Expensive. Pass arrays.
- Imports that can throw declared without
catch. Exceptions skip Rust cleanup. ReturnResultfor fallible calls.
Performance note
Adding 100,000 points with one addPoint call each took 38 ms from Rust; one addSeries call with a Float64Array took 0.6 ms.
Frequently Asked Questions
Can Rust export a class that JavaScript subclasses? JavaScript can subclass an exported class syntactically, but overridden methods are not called by Rust code; prefer composition.
How do I call an async method?
Declare it as returning js_sys::Promise and await it with JsFuture.
Can I import a default-exported class?
Use js_name = default on the type and constructor declarations.
Does web-sys use the same mechanism?
Yes — web-sys is generated extern blocks for Web IDL interfaces.
What happens if an imported JavaScript method throws?
Without catch, the exception unwinds through Wasm to the outer caller; declare the import with catch to get a Result in Rust.
Does storing a class instance in Rust keep it alive? Yes, until the Rust value is dropped; long-lived Rust references keep the JavaScript object and anything it references alive.
Related
- Importing JavaScript modules into Rust — module imports in general.
- Passing JS objects to Rust with wasm-bindgen — JsValue and casting.
- Exposing getters and setters from Rust — the reverse direction.
- Calling web APIs from Rust with wasm-bindgen — web-sys classes.
← Back to wasm-bindgen Deep Dive