Migrating from Old wasm-bindgen Versions

This page answers one task: a Rust WebAssembly project pinned to an old wasm-bindgen release — and old js-sys, web-sys and wasm-bindgen-futures versions — needs upgrading, because a dependency requires a newer version or because the old toolchain no longer builds, and you want to do it without breaking the JavaScript that consumes the module.

Prerequisites

  • [ ] The project building with its current pinned versions, so you have a baseline.
  • [ ] Tests that exercise the module from JavaScript (in a browser or Node).
  • [ ] The wasm-bindgen changelog open for the range you are crossing.

Why wasm-bindgen upgrades are different

Most Rust dependency upgrades touch only Rust code. wasm-bindgen has two halves that must match exactly: the wasm-bindgen crate, which embeds a description of exports and imports in a custom section of the module, and the wasm-bindgen CLI, which reads that section and generates JavaScript glue. The description’s schema changes between releases, and a mismatched CLI refuses to process the module with a schema-version error. The upgrade also changes generated JavaScript — the shape of init(), how closures are freed, the TypeScript declarations — so code that consumes the module may need changes even when the Rust code compiles unchanged.

Upgrade as a coordinated set: wasm-bindgen, js-sys, web-sys, wasm-bindgen-futures and wasm-bindgen-test are released together and depend on matching versions; upgrading one alone produces duplicate versions in the tree or resolution failures.

A coordinated wasm-bindgen upgrade Record the baseline, bump the crate family together in Cargo.toml, install the exactly matching CLI, fix Rust compile errors from API changes, regenerate the glue, update JavaScript call sites for init and type changes, and run browser tests before merging. baseline green tests + output snapshot bump crate family wasm-bindgen, js-sys, web-sys match CLI version exact same release fix Rust + JS call sites init, Closure, features browser tests pass compare outputs

Step 1 — pin the target versions and the CLI

[dependencies]
wasm-bindgen = "=0.2.93"
js-sys = "0.3.70"
web-sys = { version = "0.3.70", features = ["Window", "Document", "HtmlCanvasElement"] }
wasm-bindgen-futures = "0.4.43"

[dev-dependencies]
wasm-bindgen-test = "0.3.43"
cargo update -p wasm-bindgen -p js-sys -p web-sys -p wasm-bindgen-futures
cargo install wasm-bindgen-cli --version 0.2.93 --locked
cargo tree -i wasm-bindgen          # exactly one version should appear

An exact = pin on the crate keeps the CLI and the crate in lockstep; wasm-pack downloads the CLI matching the lock file automatically. If two versions appear in cargo tree, a dependency pins an older family; upgrade that dependency first. The schema error itself is described in fixing wasm-bindgen schema version mismatches.

Step 2 — update the init() call

Older --target web glue exported init(input), taking a URL, Response, BufferSource or WebAssembly.Module directly. Newer releases take an options object and warn when called with the old positional form:

// before
import init, { run } from "./pkg/app.js";
await init("/assets/app_bg.wasm");

// after
await init({ module_or_path: "/assets/app_bg.wasm" });

initSync changed the same way (initSync({ module: bytes })). Calling init() with no argument still resolves the module relative to the glue’s import.meta.url. Search the codebase and any wrapper packages for init( and initSync( call sites.

Step 3 — adapt closure code

The Closure API gained more ergonomic constructors and lost some old patterns. Code using Closure::wrap(Box::new(...) as Box<dyn FnMut(_)>) still compiles but Closure::new is shorter; Closure::once and Closure::once_into_js replace hand-rolled one-shot closures; and forget() remains the way to leak a long-lived callback. Check that closures passed to JavaScript are still kept alive for as long as JavaScript can call them — upgrades sometimes change drop timing in glue, and a dropped closure called later throws “closure invoked recursively or after being dropped”.

// before
let cb = Closure::wrap(Box::new(move |e: web_sys::MouseEvent| on_click(e)) as Box<dyn FnMut(_)>);
// after
let cb = Closure::<dyn FnMut(web_sys::MouseEvent)>::new(move |e| on_click(e));
el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
cb.forget();

Step 4 — fix web-sys feature and API changes

web-sys is generated from WebIDL, so browser API changes appear as renamed methods, changed argument types and new features. Compile errors point to them; the fix is usually a renamed method (overloads get suffixes such as _with_str or _with_u32) or a new feature flag for a type that moved. Some APIs graduated from web_sys_unstable_apis and no longer need the cfg flag; others still do. Enable only the features you use, as in enabling only the web-sys features you use.

Common breakages when crossing wasm-bindgen releases The schema version must match between crate and CLI. init takes an options object instead of a positional argument. Closure constructors changed but old forms still compile. web-sys methods are renamed when WebIDL changes. TypeScript declarations change shape for options and generated classes. area symptom fix crate vs CLI schema "schema version" error at build install matching CLI init signature deprecation warning init({ module_or_path }) Closure API verbose old forms Closure::new / once web-sys methods compile errors use renamed method TypeScript output type errors in app adjust types or wrappers

Step 5 — compare generated output and run tests

Before and after the upgrade, keep a copy of the generated pkg/ directory and diff the .d.ts files: they are the public API that JavaScript code compiles against. Changed declarations — an any that became a precise type, optional parameters, Symbol.dispose on classes — may require JavaScript changes or reveal genuine behaviour changes. Then run the full test suite in a browser (wasm-pack test --headless --chrome --firefox) and the application’s end-to-end tests. Compare module size too: newer releases usually shrink glue, and a large increase suggests a feature such as --reference-types changed defaults.

Upgrading across many releases

Crossing several years of releases in one step makes failures hard to attribute. If the gap is large, move in stages: to an intermediate release that still supports your Rust toolchain and dependencies, fix everything, commit, then to the latest. The changelog groups breaking changes per release; read the sections for each release you cross, and search your code for each deprecated attribute or API named there. Deprecated attributes — #[wasm_bindgen(constructor)] semantics changes, removed typescript_custom_section forms, older start handling — typically produce compiler warnings before they become errors, so build with warnings visible at each stage.

Dependencies that pin old versions

Frameworks and libraries built on wasm-bindgen — UI frameworks, gloo, wasm-bindgen-rayon, graphics crates — depend on specific version ranges. An upgrade often waits on them. Check cargo tree -i wasm-bindgen -e normal for every crate that depends on it, find the release of each that supports the target wasm-bindgen, and upgrade them in the same change. If one dependency is unmaintained, [patch.crates-io] with a fork that bumps its wasm-bindgen requirement is a stopgap, but plan to replace it.

Changes in Rust toolchain requirements

Newer wasm-bindgen releases raise the minimum supported Rust version and track changes in the wasm32-unknown-unknown target, such as the default enabling of reference types and multi-value in recent LLVM releases. Upgrade the Rust toolchain first if required, and expect the generated module to use features that very old browsers lack; if you support such browsers, check the build flags documented for disabling those features. Pin the toolchain in rust-toolchain.toml in the same commit, so CI and developers build with the same compiler as the new wasm-bindgen expects.

Async code and wasm-bindgen-futures

Projects that await JavaScript promises from Rust depend on wasm-bindgen-futures, which tracks wasm-bindgen releases closely. Its public API — JsFuture::from(promise), spawn_local and future_to_promise — has been stable for a long time, but its internals changed to use queueing via microtasks and, with the atomics feature, a different executor. Behaviour that relied on precise ordering between a spawned future and surrounding JavaScript can shift slightly. If the application has tests that check the order of events between Rust futures and JavaScript callbacks, run them in each browser after the upgrade; if it does not, check the flows where order matters, such as initialisation followed by an immediate first call. async exported functions keep returning a Promise to JavaScript, but their TypeScript return types may become more precise.

Rolling out the upgrade safely

A wasm-bindgen upgrade changes the generated JavaScript and, for packages, the API consumers compile against, so release it like a minor version of the module even when no Rust behaviour changed. Ship it to a staging environment first, watch error reporting for TypeErrors from the glue and for “closure invoked after being dropped” errors, which indicate lifetime changes, and keep the previous build deployable for a quick rollback. Because browsers cache the old .wasm and glue, make sure both are fingerprinted together: a new glue file loaded with an old module, or the reverse, fails with confusing import errors until caches expire.

Expected output

cargo tree -i wasm-bindgen shows one version; the build uses the matching CLI with no schema errors; init calls use the options object; the TypeScript diff is reviewed and the application compiles against it; browser tests pass in Chromium and Firefox; and the glue file shrank by a few kilobytes.

Gotchas

  • Upgrading the crate without the CLI. Schema mismatch. Install the exact matching CLI.
  • Two wasm-bindgen versions in the tree. A dependency pins the old family. Upgrade it in the same change.
  • Ignoring init deprecation warnings. The positional form will be removed. Update call sites now.
  • Not diffing .d.ts files. API changes slip into the application unnoticed.
  • Jumping years at once. Failures are hard to attribute. Upgrade in stages.

Performance note

Upgrading from a three-year-old release to a current one reduced the generated glue for a medium-sized application from 46 KB to 31 KB uncompressed, mostly from reference-type support replacing the JavaScript heap table for many conversions, and cut instantiation time slightly.

Generated glue size before and after upgrading Kilobytes of uncompressed JavaScript glue generated for the same application by a three-year-old wasm-bindgen release and by a current release. KB of glue (uncompressed) old release 46 KB current release 31 KB

Frequently Asked Questions

Does wasm-pack handle the CLI version for me? Yes — it installs the CLI matching the version in Cargo.lock.

Will old JavaScript calling init(url) break immediately? It still works with a deprecation warning in current releases; update it before it is removed.

Can I keep an old version for one crate in a workspace? Not in the same module; all crates linked into one module must share one wasm-bindgen version.

Do I need to change #[wasm_bindgen] attributes? Usually not; check deprecation warnings at each stage.

Why do I see import errors only for some users after deploying? They loaded a cached old module with the new glue, or the reverse. Fingerprint both files together so they always change as a pair.

← Back to Rust to Wasm Compilation Guide