Fixing Crates That Fail to Compile for Wasm
This page answers one task: cargo build --target wasm32-unknown-unknown fails somewhere deep in the dependency
tree, in a crate you never added directly — find out which one, why, and what to change so the build succeeds and
the module still works.
Prerequisites
- [ ] A crate that builds natively and fails for
wasm32-unknown-unknown. - [ ]
cargo tree(built into cargo) and optionallycargo-udepsorcargo-machetefor unused dependencies. - [ ] The full error output — not just the last line.
Why dependency trees break on Wasm
Most Rust crates are written for operating systems. They open sockets, spawn threads, read the clock, call C
libraries, or ask the OS for random numbers. On wasm32-unknown-unknown there is no operating system, so a crate
that depends on any of those either fails to compile — because a platform module is missing — or compiles and fails
at runtime. The first kind is the subject of this page, and it is the friendlier of the two.
The failures cluster into a few families. Crates that need randomness depend on getrandom, which refuses to
compile for an unknown platform unless told where randomness comes from. Networking crates depend on mio or on
socket APIs that do not exist. Crates with -sys in their names compile C code with a native C compiler and link
against system libraries. And some crates simply have compile_error! for unsupported platforms. Identifying the
family usually tells you the fix.
Step 1 — read the first error, not the last
Cargo compiles dependencies in parallel, and the last error printed is often a consequence of the first. Re-run with a single job so the output is in order:
cargo build --target wasm32-unknown-unknown -j1 2>&1 | grep -m1 -B2 -A12 '^error'
Compiling getrandom v0.2.15
error: the wasm*-unknown-unknown targets are not supported by default, you may need to enable the "js" feature.
For more information see: https://docs.rs/getrandom/#webassembly-support
--> /home/dev/.cargo/registry/src/.../getrandom-0.2.15/src/lib.rs:342:9
The Compiling line just before the error names the guilty crate. That is the one to investigate — not your own
code, and usually not the crate at the top of your Cargo.toml.
Step 2 — find who pulls it in
Ask cargo who depends on the failing crate, for the target you are building:
cargo tree --target wasm32-unknown-unknown -i getrandom
getrandom v0.2.15
└── rand_core v0.6.4
└── rand v0.8.5
└── uuid v1.10.0
└── app v0.1.0
-i inverts the tree to show paths to the crate. Here, generating UUIDs pulls in rand, which needs randomness.
Knowing the path matters for the fix: sometimes the right answer is a feature on uuid, not anything to do with
getrandom directly. If several paths lead to the failing crate, fix it once at the root rather than per path.
Step 3 — fix getrandom by choosing a backend
getrandom deliberately refuses to guess. In a browser the right source of randomness is
crypto.getRandomValues, which requires JavaScript glue. Enable it explicitly. For getrandom 0.2:
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
For getrandom 0.3, the backend is selected with a configuration flag as well as a feature:
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
getrandom = { version = "0.3", features = ["wasm_js"] }
# .cargo/config.toml
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
Adding getrandom as a direct dependency only to set its feature is normal and intended: cargo unifies features
across the tree, so your crate enabling it makes it available to rand and uuid too. If both 0.2 and 0.3 appear in
the tree, configure both. The randomness story in full, including why Wasm has no entropy of its own, is in
generating secure random numbers in Wasm.
Step 4 — feature-gate what the browser cannot do
For crates that need sockets, threads or files, the usual fix is to stop compiling that code for Wasm. Many crates put such functionality behind features; turn them off for the Wasm target:
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["json"] }
tokio = { version = "1", default-features = false, features = ["sync", "macros"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "net", "fs"] }
reqwest is a good example of a crate that adapts itself: on wasm32 it uses the browser’s fetch through
web-sys instead of hyper and sockets, as long as you avoid native-only features such as rustls-tls. tokio’s
sync and macros features compile for Wasm; its runtime, networking and file system do not. In a browser, futures
run on the JavaScript event loop through wasm-bindgen-futures instead.
In your own code, gate the native paths with cfg attributes:
#[cfg(not(target_arch = "wasm32"))]
pub fn load_config() -> Config { Config::from_file("config.toml") }
#[cfg(target_arch = "wasm32")]
pub fn load_config() -> Config { Config::default() } // the page passes overrides via JS
Step 5 — replace native -sys crates
A crate that compiles C code — openssl-sys, libz-sys, ring in older versions — needs a C compiler that targets
WebAssembly and a C standard library for it. wasm32-unknown-unknown has neither by default. Sometimes you can make it
work with clang and the WASI SDK’s sysroot, but the robust fix is a pure-Rust alternative:
# instead of flate2 with the zlib backend
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
# instead of native-tls / openssl
rustls = { version = "0.23", default-features = false, features = ["ring"] }
When no alternative exists and you control the fork, [patch.crates-io] lets you point a dependency at a fixed
version while you send the change upstream:
[patch.crates-io]
some-crate = { git = "https://github.com/you/some-crate", branch = "wasm32-support" }
Keep the patch temporary and tracked. A forgotten patch pins you to a stale fork for years.
Preventing the next breakage
Once the build works, keep it working. New dependencies are the main source of regressions: someone adds a crate for a
server-side feature, it compiles fine natively, and the Wasm build breaks a week later when someone else notices.
Building the Wasm target in CI on every pull request turns that into an immediate, attributable failure. A second
habit is reviewing new dependencies with cargo tree --target wasm32-unknown-unknown -e normal before merging, which
shows exactly what a dependency would add to the browser build — often a surprising amount for a small convenience.
Expected output
After the fixes, the build succeeds and the tree shows only Wasm-compatible paths:
cargo build --target wasm32-unknown-unknown --release && \
cargo tree --target wasm32-unknown-unknown -e features -i getrandom | head -5
Finished `release` profile [optimized] target(s) in 24.18s
getrandom v0.2.15
├── getrandom feature "js"
│ └── app v0.1.0
├── getrandom feature "std"
Then check behaviour, not just compilation: call the code paths that use randomness or the network from a test in a
headless browser, as in running Wasm tests in headless browsers.
A build that compiles but panics on Instant::now() is not fixed.
Gotchas
- The feature fix works locally and fails in CI. Another job built with a different feature set, or
Cargo.lockresolved a second major version of the crate. Checkcargo tree -dfor duplicates. cargo checkpasses,cargo buildfails. Linking happens only on build.-syscrates often fail at link time with an undefined-symbol error; see fixing undefined symbol errors from wasm-ld.- Dev-dependencies break the build. Test-only crates are compiled for
cargo test --target wasm32-…. Gate them by target in[target.'cfg(...)'.dev-dependencies]. - Fixing the symptom in the wrong crate. Patching a deep dependency when a feature on your direct dependency would
have removed the path entirely. Read the
cargo tree -ioutput before editing anything.
Performance note
Removing native-only dependency paths also shrinks the module. On one service crate shared with a browser client,
turning off default features on reqwest, tokio and chrono for the Wasm target cut the release module from 1.9 MB
to 740 KB before wasm-opt, because most of the removed code had been compiled in even though it could never run.
Frequently Asked Questions
Can I compile C dependencies for wasm32-unknown-unknown at all?
Yes, with clang targeting wasm32 and headers from a sysroot such as the WASI SDK’s, as long as the C code does not
need OS services at runtime. It is fiddly; a pure-Rust crate is almost always easier.
Why does the error mention js when I am not using JavaScript?
In a browser, the only cryptographically secure randomness is crypto.getRandomValues, a JavaScript API. The feature
name refers to that source.
Does building for WASI avoid these problems?
Some of them. On wasm32-wasip1, getrandom, clocks and file access work through WASI imports. Networking and threads
are still limited, and -sys crates still need a WASI-targeting C toolchain.
How do I stop this happening again? Add the Wasm build to CI so a new dependency that breaks it fails a pull request rather than surprising you later.
Related
- Choosing a Rust Wasm target triple — why
unknown-unknownhas no OS services. - Sharing validation logic between server and browser — the shared-crate setup where this comes up most.
- Awaiting JavaScript promises from Rust — async without tokio’s runtime.
- Enabling only the web-sys features you use — the browser-side counterpart to trimming features.
← Back to Rust to Wasm Compilation Guide