Reporting Wasm Crashes to an Error Tracker

This page answers one task: a WebAssembly module occasionally traps or panics for real users, and every such failure should reach your error tracker with enough context — message, symbolicated stack, build, inputs’ shape — to fix it, without flooding the tracker or leaking user data.

Prerequisites

Why Wasm crashes are under-reported

Error trackers capture uncaught exceptions automatically, but Wasm failures often do not look like interesting exceptions. A Rust panic arrives as RuntimeError: unreachable with no message; the actual panic text went to console.error and is lost. A trap inside a worker never reaches the page’s global error handler. A wrapper that catches the error to show a friendly message may swallow it entirely. And when traps are captured, they group badly: every Rust panic has the same message and similar frames, so dozens of different bugs collapse into one issue.

Reporting Wasm crashes well means capturing them deliberately at the boundary, attaching what the tracker cannot see on its own — the panic message, the module build, the operation that failed — and making sure the stack can be symbolicated on arrival.

From a trap to an actionable report A panic hook records the panic message. The trap surfaces as a RuntimeError at the wrapper, which combines it with the message, the build id, the operation name and breadcrumbs, and sends it to the error tracker, which symbolicates the Wasm frames with the uploaded debug file. panic hook records the message trap → RuntimeError caught at the wrapper enrich build id, operation, breadcrumbs send to tracker captureException symbolicate + group debug file by build id

Step 1 — capture the panic message before the trap

In Rust, install a panic hook that stores the message where JavaScript can read it, in addition to logging it:

use std::panic;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(module = "/js/crash.js")]
extern "C" { fn record_panic(message: &str); }

#[wasm_bindgen(start)]
pub fn start() {
    panic::set_hook(Box::new(|info| {
        record_panic(&info.to_string());                    // "panicked at src/header.rs:88:23: index out of bounds …"
        console_error_panic_hook::hook(info);               // still log it for developers
    }));
}
// js/crash.js
export let lastPanic = null;
export function record_panic(message) { lastPanic = message; }

For C and C++ under Emscripten, override Module.onAbort to capture the abort reason, and route assertion failures through it.

Step 2 — catch traps at the wrapper and report them

import * as Sentry from "@sentry/browser";
import { lastPanic } from "./crash.js";

export async function decode(bytes) {
  Sentry.addBreadcrumb({ category: "wasm", message: "decode", data: { size: bytes.length } });
  try {
    return engine().decode(bytes);
  } catch (err) {
    if (err instanceof WebAssembly.RuntimeError) {
      Sentry.captureException(err, {
        tags: { wasm_build: WASM_BUILD_ID, wasm_op: "decode" },
        extra: { panic: lastPanic, input_size: bytes.length },
        fingerprint: ["wasm-panic", panicLocation(lastPanic) ?? "{{ default }}"],
      });
      resetEngine();                                        // the instance may be inconsistent
    }
    throw err;
  }
}

The fingerprint groups reports by the panic’s source location (src/header.rs:88) rather than by the generic unreachable message, so different bugs become different issues. Resetting the engine follows the advice in recovering a module after a trap.

Step 3 — report from workers too

Errors inside workers do not reach the page’s handlers. Either initialise the tracker’s SDK inside the worker (most SDKs support worker scope) or catch errors in the worker and post them to the page, which reports them with the same enrichment:

// worker
self.addEventListener("error", (e) => self.postMessage({ type: "crash", message: e.message, stack: e.error?.stack, panic: lastPanic }));

Include the worker’s purpose as a tag, so a crash in the image-encoding worker is distinguishable from one in the search worker. Unhandled Promise rejections in workers need the same treatment through the unhandledrejection event.

Context to attach to a Wasm crash report Each report should carry the Wasm build id for symbolication, the panic or abort message, the operation that failed, the input's size and type but not its content, the browser and engine, and breadcrumbs of recent calls. User content and identifiers should be excluded. field example why build id app@2026.10.02+3f9a1c2 pick the debug file panic message index out of bounds … header.rs:88 the actual cause operation decode which entry point input shape size 48211, image/png reproduce without data breadcrumbs last 20 Wasm calls what led up to it

Keep the forwarding code tiny and dependency-free, so it still works when the worker’s main logic has failed to load, and give each forwarded crash a timestamp so the page can order it with its own breadcrumbs.

Step 4 — upload debug files during the build

Error trackers symbolicate Wasm frames only if they have the debug file for the exact build. Upload it in CI right after building:

# example with Sentry's CLI; other trackers have equivalent tooling
sentry-cli debug-files upload --include-sources debug/app.debug.wasm

Make the upload part of the release job and fail the job if it fails. Upload JavaScript source maps for the glue in the same step, so whole stacks — glue and Wasm frames — read cleanly.

Step 5 — control noise and protect privacy

Rate-limit reports per session and per issue, since a crash in a per-frame function can fire sixty times per second and drown out every other report. Use sampling for high-volume apps, but keep full sampling for crashes in critical operations. Never attach the input itself — documents, images and text belong to users — only its size, type and a hash if you need to correlate. Scrub panic messages for accidental data, since a format! that includes a value can leak user content into the tracker; configure the SDK’s beforeSend hook to apply a scrubber to the panic field.

Load failures are crashes too

Not every Wasm failure happens inside a call. The module can fail to load: a network error fetching the .wasm, a CompileError because the browser lacks a feature the build uses, a LinkError because the glue and the binary come from different builds after a partial deploy, or an out-of-memory error instantiating on a constrained phone. These are often more common than traps, and they disable the feature entirely for the affected users. Wrap initialisation in the same reporting path, tagging the error class and the browser and engine version: a spike of CompileError from one Safari version points at a feature-detection gap, while LinkError right after a release points at caching or deploy ordering. Report the fallback path taken, too, so you can see how many users run without the Wasm feature at all and whether a release changed that number.

Reproducing crashes from reports

The goal of a report is a fix, and a fix usually needs a reproduction. Reports that carry the operation, the input’s shape and the panic location are often enough to construct a failing test directly: a PNG of that size with a truncated chunk, a CSV with that many columns and an unterminated quote. When they are not, give users a way to opt in to sharing the failing input — a “send this file to support” button in the error message — rather than ever attaching it automatically. Fuzzing the failing entry point, seeded with inputs of the reported shape, is another effective way to find the exact trigger, as in fuzzing a Wasm module. Once reproduced, add the case to the test suite so the fix stays fixed, and link the issue to the release that contains it so the tracker can confirm the crash rate drops after deployment.

Expected output

A panic in production appears in the tracker as an issue titled with the panic message, grouped by source location, tagged with the Wasm build and operation, with a symbolicated stack through both the glue and the Wasm frames — and no user content in the payload.

Gotchas

  • Generic unreachable grouping. All panics merge into one issue. Fingerprint by panic location.
  • Lost panic messages. The message only went to the console. Record it with a panic hook.
  • Worker crashes missing. Page handlers do not see them. Report from the worker or forward errors.
  • No debug files for a release. Stacks stay as offsets. Upload in CI and fail on error.
  • Ignoring load failures. Compile and link errors disable the feature silently. Report initialisation failures too.
  • User data in reports. Inputs and formatted messages can leak content. Report shapes and scrub messages.

Performance note

The panic hook added no measurable cost to normal execution. Capturing and sending one report took about 2 ms on the main thread. Rate limiting reduced report volume from a per-frame crash by 99.6% while still recording every affected session.

Distinct issues from one week of Wasm crash reports Number of distinct issues the error tracker created from the same week of Wasm crash reports, with default grouping on the unreachable message and with fingerprinting by panic location. distinct issues created default grouping 1 fingerprint by panic location 14

Frequently Asked Questions

Do I need a special SDK for Wasm? No — standard browser SDKs capture the errors; Wasm support is mostly about symbolication and grouping, configured as above.

What about server-side Wasm? Report from the host: catch traps around guest calls, attach the guest’s id and version, and use the tracker’s server SDK.

Should I report expected errors, such as invalid input? No. Report traps and unexpected failures; expected errors returned as values belong in metrics, not the tracker.

Can I capture the Wasm memory state? It is large and may contain user data. Capture targeted values in the panic hook instead.

How do I alert on Wasm crashes? Alert on the rate of new issues tagged with the Wasm build, and on crash-free session percentages per release, rather than on single events.

← Back to Observability & Error Reporting