Quick answer: GameMaker HTML5 transpiles your GML to JavaScript that runs on the browser main thread, so crashes appear as console exceptions in mangled generated code. Install window.onerror and an unhandledrejection listener before the runner boots, attach room and object breadcrumbs from GML, and upload the source maps the exporter emits so production stacks deminify back to something you can act on.
When you export a GameMaker Studio 2 project to HTML5, your GML is transpiled to JavaScript and runs against the browser canvas, so a crash no longer looks like the familiar runner abort dialog. Instead it surfaces as a thrown exception in the developer console, often deep inside generated code with function names you never wrote. Async events make this worse, because an error inside an Async HTTP callback is detached from the gameplay that triggered it. This post explains how to make those failures legible, how source maps and breadcrumbs turn a minified blob into a fixable report, and how to keep the pipeline honest across heavily cached browser builds.
How GML failures surface in the browser
The HTML5 target compiles your GML into JavaScript that runs on the page main thread. A division by zero, an out of range array access, or a call to a missing instance method becomes a thrown JavaScript Error, and unless you intercept it the runner prints a stack to the console and stops advancing your step events. Because the generated function names are mangled, the trace rarely points at the GML line you actually wrote, so the raw console output looks like noise from a library you never authored rather than your own gameplay logic.
Async events compound the problem. HTTP requests, in app purchase callbacks, and audio loading all complete on later ticks, so an error raised inside an Async HTTP event is separated in time from the code that started it. The console shows the failure, but the temporal gap means you cannot infer the cause from the stack alone. You need the room, the object, and the event name captured at the moment the async callback ran, or you will be guessing at which interaction triggered a crash that arrived seconds after the player moved on.
Hooking window.onerror and unhandledrejection
The two browser hooks that matter are window.onerror and an event listener for unhandledrejection. The first fires for synchronous throws during a step or draw event; the second fires when a promise from fetch or a loaded extension rejects without a catch. Register both before the runner boots, ideally from a small script injected ahead of the generated GameMaker bundle, so you never miss an early failure during asset preload. A crash while the runner is still downloading sprites is otherwise completely invisible, because no handler exists yet to catch it.
From inside GML you can enrich these reports by maintaining a tiny breadcrumb buffer: push the current room name, the active object, and the last input event into a global structure on each step. When onerror fires, your JavaScript handler reads that buffer through the runner bridge and attaches it to the payload. That context is the difference between a mangled minified frame and an actionable report, because it tells you the player was in the boss room pressing fire when the array index went out of bounds, which is most of the work of reproducing the bug.
Source maps and deminification
GameMaker can emit JavaScript with accompanying source maps when you build for HTML5, and without them every frame in a production report is a meaningless column offset into a minified blob. Treat the map files as build artifacts: archive them per version and upload them to your crash backend so incoming stacks deminify automatically. Never serve the maps publicly from your game host, since they expose your full transpiled logic to anyone who opens the network tab, and you gain nothing from making them downloadable by players.
Even deminified, the names map to generated functions rather than your GML scripts, so keep a lookup from generated symbol prefixes to the script and event they originate from. Most failures cluster in a handful of objects, so a short curated table covering your hot objects pays for itself the first time a player reports a freeze during combat. Over a few releases that table becomes the fastest path from a console stack to the exact create or step event you need to open in the editor.
Browser and device variance
HTML5 means you ship to every browser at once, and the same GML can pass on desktop Chrome and fault on mobile Safari because of WebGL context limits, audio autoplay policies, or differing typed array behavior. Capture the user agent, the device pixel ratio, and whether the WebGL context was lost, because a context loss after a tab switch looks identical to a crash from the player perspective but needs an entirely different fix. Treating those two as the same bug wastes days chasing a logic error that does not exist.
Memory is the other variance axis. Mobile browsers reclaim background tabs aggressively, so a long session can hit an out of memory abort that never reproduces on your workstation. Recording the peak texture page count and the number of active audio buffers when a failure occurs lets you separate genuine logic bugs from resource pressure that only manifests on constrained hardware. Without that, a crash that only happens on cheap Android phones after twenty minutes of play looks random and unreproducible instead of being an obvious memory budget problem.
Setting it up with Bugnet
Add the Bugnet web SDK as an extra script tag on the page that hosts your exported index.html, and call its init with your project key before the GameMaker runner starts. The SDK installs its own onerror and unhandledrejection listeners, so it captures both synchronous GML throws and detached async rejections without you wiring each event by hand, and its in game report button can also capture current game state when a player hits a non fatal bug. Source map upload is a build step you point at the JavaScript and map files the exporter emits.
Once reports land, Bugnet folds them together with occurrence grouping, so a thousand players hitting the same array index fault collapse into one issue with a count, affected browser and OS breakdowns, and the breadcrumb trail you attached as custom fields. You can filter by game version, which matters because HTML5 builds are aggressively cached and your players may still be running last week bundle long after you ship a fix. Sorting by occurrence count tells you which crash is actually costing you sessions rather than which one happened to come in most recently.
Operating it across releases
Because browsers cache the exported bundle, your error volume for a given version decays slowly and overlaps with the next release. Always stamp each build with a version string in the page and forward it on every report, so your dashboard can attribute a spike to the build that actually caused it rather than smearing it across the rollout window. A cache busting filename on the data files when you ship a critical fix shortens that overlap and gets players onto the patched build faster.
Review the top signatures after each export. The distribution shifts as you fix the loud bugs and players upgrade browsers, and an error that was rare last month can dominate after a platform updates its WebGL implementation. A short post release check of ingestion health and signature churn catches a broken cache or a missing source map upload before your players do, which on the web is the difference between a quiet patch and a wave of one star reviews citing a blank canvas.
On HTML5, source maps and breadcrumbs are the whole game. Upload your map files every export, stamp the build version into the page, and capture room and object context from GML so a minified browser stack becomes a report you can actually fix.