Troubleshooting Common Wasm Errors
WebAssembly errors are terse. A browser prints CompileError: expected magic word 00 61 73 6d, found 3c 21 44 4f, a runtime prints RuntimeError: unreachable, a build tool prints a schema version mismatch, and each message describes the symptom at the exact point the engine noticed it — which is
often several steps away from the cause. A missing file shows up as a compile error about magic bytes. A version mismatch in generated glue shows up as a
missing import. A bad pointer computed in JavaScript shows up as an out-of-bounds trap deep inside memcpy. Knowing how to read the message, and which stage of the pipeline produced it, is most of the
work of fixing it.
This topic is a field guide to those messages. It groups errors by the stage at which they occur — loading, compiling, linking, running, building, and policy enforcement — explains what each stage checks, and links to a guide for each common failure with the diagnosis steps and fixes that apply. The goal is to turn an unfamiliar error into a known category within a minute, and then into a fix.
Prerequisites
- [ ] Browser DevTools (Console and Network panels), and
curlfor checking responses outside the browser. - [ ]
wasm-toolsor WABT (wasm-objdump,wasm-validate) for inspecting binaries. - [ ] A build of the failing module with function names or debug information, when the error is a runtime trap.
Errors by stage
Every WebAssembly module goes through the same pipeline: the bytes are fetched, compiled (validated and translated to machine code), linked with imports during instantiation, and then executed. Build tools add stages before that — compiling source, linking objects, generating glue — and security policies add checks around compilation. Each stage raises its own kind of error, and the JavaScript error type tells you which stage failed before you read the message at all.
The mapping is reliable enough to use as a first triage step:
TypeErrorduringinstantiateStreamingorcompileStreaming— the response, not the module: MIME type, CORS, a failed fetch.CompileError— the bytes: not a module at all (HTML, compressed data), truncated, or using a feature this engine lacks.LinkError, or aTypeErrormentioning an import — the import object: a missing namespace, a missing function, a memory or table that does not match its declaration.RuntimeError— a trap during execution:unreachable(often a panic or abort), out-of-bounds memory access, integer divide by zero, an indirect call signature mismatch, or stack exhaustion.RangeError: Maximum call stack size exceeded— the engine’s native stack, usually deep recursion.- A CSP violation message — the page’s Content Security Policy forbids compilation.
- Errors printed by build tools — toolchain mismatches, undefined symbols, unsupported crates.
Loading and compiling: is it even a module?
Loading failures are the most common in production, because they depend on servers, CDNs and deployments rather than code. The first question is always
whether the browser received the right bytes. The compile error’s “found” bytes answer it: 3c 21 44 4f is <!DO, the start of an HTML page — a 404 or a
single-page-app fallback for a path that does not exist. Compressed data that was not decoded, or a JSON error body, look different but are just as
recognisable. If the bytes are right but the header is wrong, streaming compilation refuses with a MIME-type TypeError. If the bytes start correctly
but end early, compilation fails with an “unexpected end” message. And if everything about the delivery is right, the module may use a feature — SIMD,
threads, exception handling, garbage collection, tail calls — that this engine does not support, which the error reports with an offset and an opcode.
Linking: does the import object match?
A module’s import section is a contract, and instantiation checks it entry by entry. Messages such as Import #0 "env": module is not an object or function or function import requires a callable mean the import object does not provide what the module declares. In hand-written loaders that is
usually a namespace typo or a missing function; with generated glue, it almost always means the .js and the .wasm came from different builds — a
stale cache, a partial deployment, or a CLI version that does not match the crate. Memory and table imports add type checks: limits, sharedness and
element types must match. The fix starts with listing the module’s imports with WebAssembly.Module.imports() or wasm-tools print and comparing them
with what the loader provides.
Running: what a trap is telling you
Traps are the WebAssembly equivalent of a crash, but contained: the engine stops the current call and throws a RuntimeError, and nothing outside the
module is affected. The message names the trap kind. unreachable is the most common and the least informative on its own, because compilers emit the
unreachable instruction for panics, aborts, failed assertions and impossible branches; the panic hook or abort handler message that precedes it is what
explains it. memory access out of bounds means an address past the end of linear memory — typically a very wrong pointer, not a full memory. integer divide by zero and integer overflow come from integer division and float-to-int conversions. indirect call signature mismatch means a function
pointer was called with the wrong type. After any trap the instance may be in an inconsistent state, so recovering means re-instantiating, as described in
recovering a module after a trap.
Build-time errors: the toolchain is part of the program
Some of the most confusing failures happen before anything runs. The wasm-bindgen CLI refuses to process a binary built against a different crate
version. wasm-ld reports undefined symbols that native linkers would have resolved from system libraries. Crates fail to compile for wasm32 because a
dependency assumes an operating system, threads or a random-number source. After upgrades, a new compiler may enable instructions by default that older
browsers reject, or an optimiser may expose undefined behaviour that used to be harmless. These errors share a theme: a WebAssembly build is a chain of
tools whose versions and settings must agree, so pinning versions and upgrading one component at a time are the main preventive measures. Guides in the
compilation section cover the individual tools; this topic covers the errors that send people searching.
Policy errors: when the page forbids compilation
Content Security Policy treats WebAssembly compilation as code generation. A policy whose script-src lacks 'wasm-unsafe-eval' blocks every way of
compiling a module, with a console message naming the directive. The right fix is that specific keyword, not the much broader 'unsafe-eval'. Browser
extensions, Electron apps and pages behind security proxies are where this most often appears, because their policies are stricter by default or set by
teams who do not work on the WebAssembly feature.
A method that works for any error
When an error matches none of the guides below, the same four questions get to the cause quickly. Which stage failed? Use the error type. What exactly did that stage receive? For loading, the response status, headers and first bytes; for linking, the import list and the import object; for traps, the stack trace with names and the arguments of the call. What changed? A deployment, a dependency update, a browser release, a new input. Can it be reproduced in isolation? A minimal page, a single test, a reduced input or module. Most WebAssembly errors fall to the first two questions; the last two handle the rest. Keep notes of what each error turned out to be — a short internal page mapping messages to causes in your own system saves the next person the same search.
Errors outside the browser
Node.js, Deno and Bun use the same engines as browsers, so the error types and most messages are identical, but the causes shift. Loading problems become
file-path problems: a readFile relative to the working directory instead of the module, a .wasm file left out of the published npm package, or a
bundler that inlined the glue but not the binary. Linking problems often involve WASI: a module built for wasm32-wasip1 needs a WASI implementation,
and node:wasi must be given the right version option. Server-side runtimes such as wasmtime and WasmEdge print their own messages, usually with more
detail than browsers — a trap message there includes a backtrace with function names and offsets, and an import mismatch names the expected signature.
Fuel exhaustion and epoch deadlines appear as traps with specific codes, which are policy decisions by the host rather than bugs in the guest. The same
triage applies: identify the stage, inspect what it received, and look for what changed.
Collecting what a bug report needs
Whether you are reporting a problem upstream or receiving reports from users, the same small set of facts resolves most WebAssembly errors quickly: the full error message and type; the browser or runtime and its version; the URL of the module and the response’s status and headers; the first bytes of the response for loading failures; the module’s imports for linking failures; the stack trace with names, the panic or abort message and the call’s arguments for traps; and the versions of the toolchain that built the module. A loader that collects these automatically when something fails, and a build that embeds its toolchain versions in a custom section, turn vague reports into precise ones. When filing issues with toolchains or engines, include a minimal reproduction — a tiny module or a few lines of source — because maintainers can act on that immediately.
Preventing errors before they ship
Many of the errors in this topic can be caught before users see them. A deployment smoke test that requests the production .wasm URL and checks the
status, Content-Type, Content-Encoding and first bytes catches every loading failure class. A contract test that compares WebAssembly.Module.imports()
with the loader’s import object catches glue mismatches. Running the test suite in every supported browser — including the oldest Safari you support —
catches feature-support and stack-size differences. Validating the binary against your minimum feature set with wasm-tools validate catches new
instructions introduced by compiler upgrades. Sanitizer builds in CI catch the memory bugs that later surface as traps. And CSP checks in the end-to-end
tests catch policy regressions. Together these turn the most common production errors into failing checks on the change that caused them.
Errors that are not errors
A few messages look alarming but need no fix. DevTools may print warnings about wasm-unsafe-eval in report-only policies, which only means the policy
would block compilation if enforced. Some browsers log that a module “could not be cached” when the code cache rejects a response, which affects only
startup time. Emscripten and wasm-bindgen print a console warning when streaming compilation falls back to arrayBuffer, which is a performance problem
worth fixing but not a failure. And console.error output from a Rust panic hook appears even when the wrapper catches the trap and recovers. Learning to
recognise these keeps attention on the failures that matter, but each is still worth a look: a fallback warning on every load costs real startup time, and
a panic message that appears regularly in production logs points at a bug users are hitting, even if the application hides it.
Gotchas and failure modes
- Fixing the symptom. Switching from
instantiateStreamingtoinstantiatehides a MIME problem but slows every load. Fix the header. - Trusting the innermost frame. Traps often surface inside
memcpyor the allocator; the bug is in the caller. - Reusing a trapped instance. State may be half-updated. Re-instantiate.
- Mixing glue and binaries from different builds. Deploy them together with content-hashed names.
- Testing in one browser only. Feature support, stack sizes and messages differ between engines.
- Ignoring console warnings. Fallback and caching warnings point at real startup costs even when nothing fails.
- Weakening CSP too far.
'unsafe-eval're-enables JavaScript eval; use'wasm-unsafe-eval'.
Verification
After fixing an error, verify the fix at the stage that failed. For loading, curl -sI shows the right status and headers and the first bytes are 00 61 73 6d. For linking, a contract test compares the module’s imports with the import object. For traps, the reproducing input becomes a test that passes,
and the instance continues working afterwards. For build errors, a clean build in CI from the committed lockfile succeeds. For policy errors, a
securitypolicyviolation listener records no violations during an end-to-end test.
Guides in this topic
- Fixing incorrect response MIME type errors — why instantiateStreaming rejects application/octet-stream and how to fix it on every common host.
- Fixing WebAssembly.instantiateStreaming failures — network, CORS and Content-Encoding problems behind streaming compile errors.
- Fixing “Import #0 module is not an object” errors — LinkErrors from missing or misnamed import namespaces.
- Fixing memory access out of bounds traps — tracing the bad pointer behind an out-of-bounds trap back to its source.
- Fixing wasm-bindgen schema version mismatches — the CLI and crate version error, and pinning them together.
- Fixing Maximum call stack size exceeded in Wasm — deep recursion and huge frames that overflow the engine stack.
- Fixing traps that appear after a dependency upgrade — bisecting toolchain and crate upgrades.
- Fixing CSP errors that block Wasm compilation — the wasm-unsafe-eval directive and what it allows.
Frequently Asked Questions
Why do WebAssembly errors look so different between browsers?
Each engine words its messages differently, but the error types — CompileError, LinkError, RuntimeError, TypeError — are standardised. Triage by
type first.
How do I get readable stack traces for traps? Keep the name section in development builds, or symbolicate production stacks with stored debug information.
Is a trap a security problem? No — it means the sandbox stopped an invalid operation. The underlying bug may still need fixing, especially if it corrupts data before trapping.
Why does my module work locally but fail in production? Production adds CDNs, compression, base paths, caching, authentication and stricter policies. Compare the production response with the local one.
Should I catch all WebAssembly errors in the loader? Catch them to report and recover, but do not hide them: record the stage, the message and diagnostic details, and surface a useful error to the user.
Where do I start when an error is not listed here? Identify the stage from the error type, inspect what that stage received, and look for what changed recently.
Do server runtimes give better error messages? Often yes — wasmtime and WasmEdge print backtraces with names and expected signatures. Reproducing a browser failure under them can speed up diagnosis.
Which tools should every Wasm developer have installed for troubleshooting?
curl for responses, wasm-tools for printing, validating and inspecting binaries, and a browser with DevTools able to show Wasm source and memory.
How long should I keep debug builds and symbols? As long as any deployed release may still be reporting errors, typically several months for web applications.
Which error should I fix first when several appear? The earliest one in the lifecycle — a fetch or compile error causes every later failure, so later messages are often noise.
Do error messages differ between browsers? Yes, in wording, but the error type and stage are the same; search by type and stage rather than exact text.
Is a trap always a bug in the module? Usually, but bad arguments from JavaScript — an out-of-range pointer or length — trap inside correct code, so check the caller too.
Related
- Handling CompileError and LinkError — the error types in depth.
- Catching Wasm traps in JavaScript — handling runtime traps.
- Reading Wasm stack traces — making traps readable.
- Reporting Wasm crashes to an error tracker — seeing these errors in production.