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 optionally cargo-udeps or cargo-machete for 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.

The common families of wasm32 build failures Four kinds of dependency that break wasm32-unknown-unknown builds, the typical error message each produces, and the usual fix. family typical error usual fix getrandom target is not supported, for more information see … enable the wasm_js backend mio / tokio net unresolved import / no field in net feature-gate networking off native -sys crate cc failed / linking with cc failed pure-Rust alternative threads, rayon compiles, panics at runtime cfg-gate the parallel path

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.

Tracing a failing crate back to your dependency The first compile error names a deep crate. cargo tree with the inverse flag shows the path from your crate to it, which identifies the direct dependency and feature that pulls it in, and therefore where the fix belongs. first error Compiling getrandom cargo tree -i path to your crate direct dependency uuid with v4 feature fix at the root feature, cfg or alternative

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.lock resolved a second major version of the crate. Check cargo tree -d for duplicates.
  • cargo check passes, cargo build fails. Linking happens only on build. -sys crates 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 -i output 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.

Module size before and after trimming dependency features for Wasm A crate shared between a server and a browser client, built for wasm32-unknown-unknown in release mode. Disabling native-only default features on three dependencies removes code that could never run in the browser. release .wasm size before wasm-opt (KB) default features everywhere 1,910 KB reqwest without default TLS 1,340 KB + tokio sync/macros only 960 KB + chrono without clock 740 KB

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.

← Back to Rust to Wasm Compilation Guide