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
- [ ] An error tracker such as Sentry, Rollbar, Bugsnag or a self-hosted equivalent, with its browser SDK installed.
- [ ] A wrapper module through which the application calls the Wasm module, as in designing a promise-based API around a Wasm module.
- [ ] Debug files kept per build, as in symbolicating Wasm stack traces in production.
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.
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.
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
unreachablegrouping. 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.
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.
Related
- Debugging unreachable-executed traps — investigating once reported.
- Throwing JavaScript exceptions from Rust — expected errors that should not be crashes.
- Measuring Wasm performance with real-user monitoring — the performance counterpart.
- Handling panics in Rust Wasm — panic configuration.
← Back to Observability & Error Reporting