Quick answer: Haxe compiles one codebase to many runtimes, so crash handling has a shared layer and a target specific layer. Use haxe.CallStack to capture stacks portably and haxe.Exception for typed errors, then add the native handler each target needs: signals and minidumps on hxcpp, window onerror on HTML5. Tag every report with its target so you can tell a logic bug from a runtime specific failure across the platforms you ship.
Haxe's promise is one codebase, many targets: the same game logic compiled to C++ through hxcpp for native desktop, to JavaScript for HTML5, and potentially to other backends. That power complicates crash reporting, because a crash on the native target behaves nothing like a crash in the browser even though the source is shared. You need a portable layer that captures what Haxe can express everywhere, and a target specific layer that handles each runtime's real failure modes. This post covers haxe.CallStack and haxe.Exception, the per target handlers for hxcpp and HTML5, and unifying the reports so one dashboard makes sense of all of them.
One source, many runtimes
When you write a Haxe game, you write against Haxe's abstractions, but at runtime your code is C++, or JavaScript, or another backend, each with its own crash behavior. An uncaught error on the JavaScript target is a browser level event; on hxcpp it can be a thrown exception that unwinds or a hard native signal from a memory fault. The shared Haxe layer cannot see the native fault on hxcpp, and the browser's reporting cannot symbolicate the way a native debugger can. So crash handling for Haxe is genuinely two tiered by necessity.
The practical consequence is that you design your reporting around a common report shape filled by target specific capture. Whatever the target, you want a stack, a message, the build version, and context, but how you obtain the stack differs. Accepting this split up front, rather than hoping one mechanism works everywhere, is what makes Haxe crash reporting reliable. The reward is that once each target feeds the same report shape, your downstream handling, grouping and triage, is uniform even though the capture underneath is not.
haxe.CallStack and haxe.Exception
Haxe gives you portable primitives that work across targets. haxe.CallStack.callStack and exceptionStack return the current or the most recent exception's stack as a list of frames, abstracting over the underlying runtime so you get a usable trace on hxcpp and on JavaScript alike. haxe.Exception, introduced to unify error handling, wraps thrown values with a message and a native stack, so catching a haxe.Exception at your top level gives you a consistent object to report regardless of target. These are the backbone of the shared capture layer.
You typically wrap your main loop or entry point in a try and catch for haxe.Exception, and in the catch you pull the message and exceptionStack and build a report. This catches the errors that travel through Haxe's exception machinery on every target, which covers a large share of logic bugs: null access, failed casts, out of range, and your own thrown errors. What it does not catch is the failures that never become Haxe exceptions, particularly native faults on hxcpp, which is exactly why the shared layer is not enough on its own and the target specific layer has to fill the gap.
Target specific capture
On the hxcpp target you are running compiled C++, so the native failure modes apply: a memory fault is a signal, not a Haxe exception, and to capture it you install signal handlers and ideally write a minidump, the same as any native C++ game. hxcpp has its own runtime and can be configured to print stacks on crash, but for field reporting you want the structured native capture so a segfault in a C++ extern or in the generated code arrives with a real stack rather than as a silent close. This is the target where the shared Haxe layer is least sufficient.
On the HTML5 target you are in the browser, so you hook window.onerror and the unhandledrejection event to catch errors that escape your try and catch, including those from asynchronous code and from JavaScript libraries you interop with. The browser gives you a stack, though minification can obscure it unless you keep source maps. Other targets each have their analog: the point is to add, for every backend you actually ship, the one native hook that catches what Haxe's portable layer cannot, so no target has a silent failure mode that goes unreported.
Unifying reports across targets
The value of Haxe is one codebase, and your crash reporting should preserve that by funneling every target into one report shape and one destination. Each report carries the same fields, the stack, message, build version, and game state, plus one essential extra: the target it came from. Tagging the target, hxcpp or html5 or whatever else, is what lets you tell whether a crash is a shared logic bug that appears everywhere or a runtime specific failure that only one backend hits. Without that tag, a flood of mixed reports is hard to interpret.
A shared logic bug is the most efficient kind to fix, because one change in the Haxe source resolves it on every target at once, and seeing the same crash signature across targets confirms it lives in shared code. A crash that appears on only one target points instead at that runtime, an hxcpp extern, a browser quirk, a source map gap, and needs target specific attention. The unified report with a target tag is the lens that distinguishes these two cases at a glance, which is the most important diagnostic question a cross platform game can ask of its crashes.
Setting it up with Bugnet
Bugnet is a natural fit for the unified report shape Haxe needs, because it accepts crashes from any target into one dashboard with the context you attach. From your shared catch handler and your per target native hooks you send the stack, the message, the build, and a custom field for the target, so a crash from the HTML5 build and a crash from the hxcpp build sit side by side and comparable. The in game report button works across targets too, giving players on the web build and the desktop build the same way to describe a problem and send their game state.
Occurrence grouping does the cross target correlation for you: when the same crash signature arrives from multiple targets it groups into one issue with a count, and the breakdown by your target field shows immediately whether it is universal or runtime specific. That is exactly the prioritization a Haxe team needs, since a shared bug affecting all targets outranks one that hits a single backend. Filtering by target, browser, or platform turns the inherent complexity of shipping one codebase to many runtimes into a clear, ranked view of where your crashes actually live.
Testing across the matrix
A cross platform game has a test matrix, and crash reporting belongs in it. For each target you ship, deliberately trigger both a Haxe exception and a target specific failure, a native fault on hxcpp, a thrown error in async browser code on HTML5, and confirm each produces a tagged report that reaches your dashboard. The targets fail differently, so a path that works on one cannot be assumed to work on another; only per target verification proves coverage. Keep source maps for the web build and symbols for the native build so the stacks resolve.
Done well, Haxe crash reporting turns the multi target burden into an advantage. Every runtime you ship reports its own crashes in a common shape, and the grouped data tells you continuously which bugs are shared and which are platform bound. You fix the shared ones once and the platform ones with focus, and a new target you add later slots into the same pipeline. For a small team using Haxe precisely to reach many platforms cheaply, having crash visibility that is equally cross platform is what keeps that reach from becoming a maintenance trap.
Haxe crash reporting is two tiered: a portable Haxe layer plus a native hook per target. Tag the target so shared bugs and runtime bugs stay distinct.