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.
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.
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
initdeprecation warnings. The positional form will be removed. Update call sites now. - Not diffing
.d.tsfiles. 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.
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.
Related
- Fixing wasm-bindgen schema version mismatches — the most common upgrade error.
- Pinning Wasm toolchain versions — keeping versions in step.
- Passing closures between Rust and JavaScript — the Closure API.
- Reading the glue code wasm-bindgen generates — what changes between versions.
← Back to Rust to Wasm Compilation Guide