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 curl for checking responses outside the browser.
  • [ ] wasm-tools or 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.

Where WebAssembly errors come from Build-time errors come from compilers, linkers and glue generators. TypeErrors during loading come from fetch, CORS and MIME checks. CompileErrors come from validation of the bytes. LinkErrors come from matching imports at instantiation. RuntimeErrors are traps during execution. CSP violations block compilation entirely. build time rustc, emcc, wasm-ld, wasm-bindgen CLI loading — TypeError fetch, CORS, MIME type, encoding compile — CompileError invalid bytes, unsupported features link — LinkError / TypeError imports missing or mismatched run — RuntimeError traps: unreachable, out of bounds

The mapping is reliable enough to use as a first triage step:

  • TypeError during instantiateStreaming or compileStreaming — 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 a TypeError mentioning 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.

Triage for a module that will not load Check the HTTP status and Content-Type first, then the first bytes of the body. HTML or JSON bytes mean a wrong path or error response. Undecoded compression means a missing Content-Encoding. Correct magic bytes with a compile error mean truncation or an unsupported feature. status + Content-Type Network panel or curl -I first 4 bytes 00 61 73 6d ? 3c 21 / 7b 22 HTML or JSON: fix path 1f 8b / binary add Content-Encoding magic OK truncation or feature

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.

Common trap messages and what they usually mean unreachable usually means a panic, abort or failed assertion. Memory access out of bounds means a bad pointer or length. Integer divide by zero means a zero divisor. Indirect call signature mismatch means a function pointer cast to the wrong type. Stack exhaustion means deep recursion. trap message usual cause first step unreachable panic / abort / assert read the panic message memory access out of bounds bad pointer or length log pointer + length integer divide by zero zero divisor check inputs indirect call signature mismatch wrong function-pointer cast find the cast call stack exhausted deep recursion make it iterative

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 instantiateStreaming to instantiate hides a MIME problem but slows every load. Fix the header.
  • Trusting the innermost frame. Traps often surface inside memcpy or 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

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.

← Back to WebAssembly Core Concepts & Browser Runtime