Quick answer: Emscripten compiles C and C++ to WebAssembly, so crashes blend native and browser behavior. An assertion or fatal error calls abort, which you hook with the onAbort handler; a memory error becomes a WASM trap that surfaces as a JavaScript exception. Hook abort, catch traps at the JS boundary along with window.onerror, keep WASM source maps or DWARF info to symbolicate, and capture browser and memory context. Group by signature across the browser matrix.
Emscripten takes a native C or C++ game and compiles it to WebAssembly so it runs in the browser, which means the crash behavior is a hybrid: native code semantics executing inside a browser sandbox. A failed assertion calls abort the way it would natively, but the result lands in JavaScript; a bad memory access becomes a WASM trap that surfaces as a JS exception with a stack full of mangled WASM frames. Reporting these crashes means bridging both worlds. This post covers hooking abort, catching WASM traps at the JavaScript boundary, source maps for WebAssembly, and the browser and memory context that matters.
Native crashes in a browser sandbox
When you compile a game with Emscripten, your C or C++ keeps its native semantics but runs as WebAssembly inside the browser. A failed assert, an abort call, or a call to exit goes through Emscripten's runtime, which by default prints to the console and stops the module. A genuine memory error, an out of bounds access in the linear memory heap, often becomes a WASM trap, which the browser turns into a JavaScript exception that propagates out of whatever JS called into the module. So a single game has two crash flavors at once: the native style aborts and the browser style exceptions.
This hybrid nature is the key thing to understand. You cannot rely purely on native signal handling, because there are no POSIX signals in the WASM sandbox in the usual sense, nor purely on browser error handling, because Emscripten's abort path is its own mechanism. Effective reporting hooks both the Emscripten runtime's abort and the JavaScript boundary where traps surface. Recognizing that an Emscripten game fails in these two distinct ways, and covering each, is what separates a setup that catches crashes from one that catches only half of them.
Hooking abort
Emscripten exposes hooks on its Module object, and the onAbort handler is the one that fires when the runtime aborts, from a failed assertion, an unrecoverable error, or an explicit abort call. You set Module.onAbort to a function that captures the abort reason and your context and sends a report before the module is torn down. Because abort is Emscripten's way of signaling a fatal native condition, this hook catches the large class of crashes that come from your C or C++ logic deciding it cannot continue, which a browser error handler alone would not cleanly attribute to your code.
Emscripten also lets you configure assertions and stack overflow checks at build time, and with those enabled the runtime produces more descriptive aborts, telling you which assertion failed or that the stack overflowed rather than just trapping opaquely. For development builds you turn these checks on so aborts carry detail; for production you balance them against size and speed, but keeping at least the most informative checks often pays for itself in debuggability. The onAbort hook combined with informative runtime checks turns Emscripten's native fatal path into structured, attributable crash reports.
Traps at the JavaScript boundary
The other crash flavor, a WASM trap from a memory access violation, an integer divide by zero, or an unreachable instruction, surfaces as a JavaScript exception thrown out of the call into the module. That means it can be caught by wrapping your calls into the WASM module in JavaScript try and catch, and it will also reach window.onerror if it escapes. So you cover this path with both a try and catch around the main loop's call into WASM and a global window.onerror and unhandledrejection listener as a backstop, the same browser hooks any web game needs, here catching native traps instead of JS errors.
The stack you get from a trap is a mix of JavaScript frames and WebAssembly frames, and the WASM frames are by default opaque function indices rather than your C function names. Catching the exception is only half the job; making the stack readable requires the symbol information discussed next. Tagging whether a report came from onAbort, a caught trap, or window.onerror tells you which flavor of crash you are looking at, native fatal condition versus memory trap, which is the first thing you want to know when triaging an Emscripten crash.
Source maps and WASM symbols
A WebAssembly stack trace is unreadable without symbol information, just like a minified JavaScript stack. Emscripten can emit DWARF debug information into the WASM, or generate a source map, that maps WASM offsets and function indices back to your original C and C++ source files, functions, and lines. With separate dwarf or source map output, a tool or a backend can symbolicate a trap stack into your actual code. As with every platform, you keep this symbol information per build and apply it to incoming crashes rather than necessarily shipping it to players.
There is a size and tooling tradeoff: full DWARF info is large and usually kept separate from the shipped WASM, while a source map is lighter but maps less detail. Whichever you choose, the rule is the familiar one, generate the symbol artifact when you build, archive it keyed to that build, and use it to resolve field crashes. An Emscripten crash report with a symbolicated stack points at the C++ line that faulted; the same report without symbols is a list of WASM function indices that tells you almost nothing. The symbol step is what makes the whole pipeline worthwhile.
Setting it up with Bugnet
Bugnet collects both flavors of Emscripten crash into one dashboard with context attached, so an onAbort from a failed assertion and a memory trap caught at the JS boundary land together as readable, symbolicated reports once you supply the DWARF or source map for the build. From Module.onAbort, your try and catch, and window.onerror you forward the message, the layer tag, and custom fields for the browser, the WASM memory size, and your game state. The in game report button gives players a way to describe a frozen or blank canvas and send the state, which matters because a trap often leaves the canvas dead with nothing on screen.
Emscripten games inherit both native crash clustering and browser variance, and occurrence grouping handles both: the same C++ fault across many players and many browsers folds into one issue with a count and a breakdown. Filtering by browser tells you whether a crash is universal or browser specific, and filtering by your memory size field helps you spot the out of memory and heap growth failures that constrained devices hit. The occurrence counts rank the work, so you fix the abort or trap affecting the most players first rather than guessing from scattered console output you never see.
Memory growth and testing
A class of Emscripten crashes is specifically about memory. The WASM linear memory heap has a size, and if your game allocates beyond it without memory growth enabled, allocation fails and the runtime aborts; with growth enabled, the heap can grow but a constrained device may still hit the browser's limit and trap. Capturing the current and maximum heap size in your reports makes these failures legible, since an out of memory abort looks like any other abort until you see the memory was exhausted. This is a common failure on mobile browsers that native testing never reveals.
Test both crash flavors across browsers and on a real mobile device. Force an assertion failure to exercise onAbort, force an out of bounds access to exercise the trap and JS boundary path, and push memory until growth fails, confirming each produces a symbolicated, contextual report. Verify the symbol artifact actually resolves your stacks. With abort and traps both hooked, symbols archived, and memory and browser context flowing, an Emscripten game gets crash visibility that respects its dual nature, native code in a browser, so the inevitable faults arrive as fixable reports instead of silent console messages no player will ever send you.
Emscripten crashes are native faults in a browser. Hook onAbort and catch WASM traps at the JS boundary, keep DWARF or source maps, and watch heap memory.