Handling CompileError and LinkError

This page answers one task: loading a WebAssembly module can fail in several distinct ways — handle each one deliberately, show users something sensible, and report enough detail to fix the cause.

Prerequisites

  • [ ] A page or app that loads at least one module.
  • [ ] An error-reporting channel — console logs during development, an error tracker in production.

Three error types for three stages

Getting from bytes to a running module happens in stages, and the WebAssembly JavaScript API defines a distinct error type for failures in each. Knowing which stage failed is most of the diagnosis.

WebAssembly.CompileError means the bytes are not a valid module for this engine: the file is truncated or corrupted, it is not WebAssembly at all (an HTML error page served with a 200 status is the classic case), or it uses a feature the engine does not support. It is thrown by compile, compileStreaming, instantiate and instantiateStreaming, and also when a Content Security Policy blocks compilation.

WebAssembly.LinkError means the module is valid but cannot be connected to its host: an import is missing, has the wrong kind, or has an incompatible type or size. It is thrown during instantiation, after compilation has succeeded.

WebAssembly.RuntimeError means a running module trapped: an out-of-bounds memory access, an unreachable instruction (often a Rust panic), integer division by zero, a stack overflow inside Wasm, or an indirect-call signature mismatch. It can be thrown from instantiation — if the start function traps — or from any later call into an export.

Ordinary TypeErrors and network errors also appear in loading code: a failed fetch, a wrong MIME type for streaming compilation, a non-callable value where an import function was expected in some engines.

Which error each loading stage can throw Fetching can fail with a network or MIME TypeError. Compiling can fail with CompileError for invalid bytes, unsupported features or CSP. Instantiating can fail with LinkError for unsatisfied imports, or RuntimeError if the start function traps. Calls into exports can throw RuntimeError on traps. fetch TypeError: network, MIME compile CompileError: bytes, features, CSP link imports LinkError: missing or wrong import start function RuntimeError: trap calls RuntimeError: trap

Step 1 — catch by stage, not with one generic handler

export async function loadModule(url, imports) {
  let response;
  try {
    response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
  } catch (err) {
    throw Object.assign(new Error("module download failed"), { stage: "fetch", cause: err });
  }

  try {
    const { instance } = await WebAssembly.instantiateStreaming(response, imports);
    return instance;
  } catch (err) {
    const stage =
      err instanceof WebAssembly.CompileError ? "compile" :
      err instanceof WebAssembly.LinkError ? "link" :
      err instanceof WebAssembly.RuntimeError ? "start" : "other";
    throw Object.assign(new Error(`module ${stage} failed`), { stage, cause: err });
  }
}

Checking response.ok before compiling turns the most confusing failure — a 404 page compiled as WebAssembly, which surfaces as a CompileError about the magic number — into a clear HTTP error. Wrapping errors with a stage field keeps the original error as the cause for logs while giving callers something simple to branch on.

Step 2 — read the messages

Engines put specific, useful detail in each message. Recognising the common ones speeds up debugging:

CompileError: WebAssembly.instantiateStreaming(): expected magic word 00 61 73 6d, found 3c 21 44 4f @+0

3c 21 44 4f is <!DO — the start of an HTML document. The server returned a page instead of the module.

CompileError: WebAssembly.instantiateStreaming(): Compiling function #412 failed: invalid simd opcode @+18842

The module uses SIMD and the engine (or a flag, or a policy) does not support it — a feature mismatch, covered in detecting proposal support at runtime.

LinkError: WebAssembly.instantiate(): Import #7 "wbg" "__wbg_new_8a6f238a": function import requires a callable

The import object does not match the module — almost always glue from one build paired with a module from another.

Common loading errors and their usual cause Frequently seen WebAssembly loading errors, the message pattern that identifies each, and the cause behind it in most cases. error message contains usual cause CompileError magic word … found 3c 21 HTML served instead of .wasm CompileError invalid … opcode feature not supported CompileError Refused to compile … CSP missing 'wasm-unsafe-eval' LinkError Import #N … requires a callable glue and module from different builds LinkError memory import … smaller than host memory too small RuntimeError unreachable panic or abort during start

Step 3 — respond appropriately to each

Each stage calls for a different response. Fetch failures are often transient: retry with backoff once or twice, then show an offline message. Compile failures are not transient: a corrupted or unsupported module will fail the same way every time, so fall back — to a different build, a JavaScript implementation, or a “not available” state — as in shipping a JavaScript fallback for a Wasm feature. Link failures are deployment bugs: the page and the module disagree. Retrying will not help, but a reload might, if a stale cached file caused it. Runtime failures during start are bugs in the module’s initialisation; report them with full detail.

try {
  engine = await loadModule(url, imports);
} catch (err) {
  report(err);                                       // always send stage + cause to monitoring
  if (err.stage === "fetch") return showOffline();
  if (err.stage === "link") return promptReload("A new version is available. Reload to continue.");
  return useFallback();                              // compile / start failures
}

Step 4 — report enough to fix it

A report that says “CompileError” is not actionable; one that says “CompileError at @+0, found 3c 21 44 4f, URL /assets/app-3f9a1c.wasm, build 2.4.0” is. Include the stage, the engine’s message, the module URL (with its hash), the page’s build identifier, and the browser. For LinkError, include the failing import name. For RuntimeError, include the stack trace — readable if the module kept its name section, as described in reading Wasm stack traces. Error trackers group these well once the stage is a tag.

Step 5 — prevent the common ones

Most loading errors in production are configuration and deployment problems rather than bugs, and each has a cheap preventive check. Serve modules with Content-Type: application/wasm and return real 404s instead of HTML fallbacks for missing assets. Use content-hashed file names so glue and module always come from the same build — the approach in versioning Wasm files with content hashes. Include 'wasm-unsafe-eval' in your Content Security Policy. And run a headless-browser smoke test against the production build in CI that simply loads the module — it catches every one of these before users do.

Why the API distinguishes the stages

It would have been simpler to throw one generic error type from every WebAssembly call. The specification separates them because the stages genuinely mean different things, and code that handles them needs to know which happened. Compilation is a pure function of the bytes and the engine, so its failures are deterministic and say something about the file. Linking depends on what the host provides, so its failures say something about the page. Traps depend on inputs and state, so they say something about a particular run. A host that conflates them retries when it should fall back, or falls back when a reload would have fixed things. The distinct types make the right response a simple instanceof check.

Expected output

With an HTML page served at the module URL, the loader reports:

module compile failed { stage: "compile", cause: CompileError: … expected magic word 00 61 73 6d, found 3c 21 44 4f @+0 }

and the page shows its fallback instead of a blank area.

Gotchas

  • Catching everything as one error. Retrying a CompileError or falling back on a transient network error both waste time. Branch on stage.
  • Single-page-app fallbacks serving HTML for .wasm URLs. A catch-all route returns index.html for missing modules. Exclude asset paths.
  • Retrying forever on fetch failures. Cap retries and back off, or a dead CDN becomes a busy loop on every client.
  • Hiding the original error. Wrapping without cause loses the engine’s message, which is the most useful part.
  • Treating LinkError as a module bug. It is almost always mismatched files. Check caching and deployment before the code.

Performance note

Error handling costs nothing on the success path. Checking response.ok before compiling saves real time on the failure path: a 2 MB HTML error page that is fed to the compiler fails only after it has been downloaded, while the status check fails as soon as headers arrive — on a slow connection, the difference was about a second.

Time to detect a missing module Time until the loader reports failure when the server returns a 404 HTML page for the module URL, with and without checking response.ok before compiling, over a throttled connection. ms until failure is reported compile the HTML body 1,240 ms check response.ok first 180 ms

Frequently Asked Questions

Can I tell CSP failures apart from other CompileErrors? Their message mentions the Content Security Policy or unsafe-eval. Match on the message, and report it separately, because the fix is a header.

Is WebAssembly.validate a cheaper pre-check? It validates without compiling to machine code, but it still requires the whole module in memory. It is useful for uploaded files, less so for your own modules.

Can a module fail to compile in one browser and not another? Yes, when it uses a proposal one engine supports and another does not, or when it exceeds an engine-specific limit such as function size. Test in every engine you support.

Do these errors differ in Node? The same types exist. Node adds its own errors for file-system loading, such as ENOENT before compilation even starts.

Should I catch RuntimeError from every export call? Catch it at the feature boundary — where a failed operation should become a message or a retry — not around every call.

← Back to Wasm Instantiation Lifecycle