Quick answer: TypeScript web games crash through plain browser errors, but the stack traces you see are minified and useless on their own. Upload source maps, listen for window error and unhandledrejection events, capture the build hash and browser context, and group duplicates. Bugnet does the capturing and folding so you fix the real bug instead of decoding column 1 of bundle.js.
A TypeScript web game compiles down to one or two minified JavaScript bundles before it ever reaches a player. That compilation is exactly why crash reporting is harder than it looks: the runtime throws an error in your beautifully typed code, but the stack trace points at bundle.js line 1 column 84213. Without source maps and a real capture pipeline you are staring at noise. This post walks through what actually breaks in browser-hosted TypeScript games, how source maps restore the truth, which browser events you must listen to, and how to turn raw thrown errors into grouped, actionable issues you can prioritize.
Why minified stack traces lie to you
When esbuild, Vite, or Rollup bundle your game, they rename variables, inline functions, and collapse everything onto a few enormous lines. A TypeError thrown deep in your physics step arrives as a position in transpiled output that bears no resemblance to your source. You cannot tell which system failed, and two unrelated crashes can land on the same minified line, making them look identical when they are not.
The fix is the source map your bundler already produces. A source map is a lookup table from generated positions back to original TypeScript file, line, and column. If you upload that map alongside the build and resolve traces against it, the same crash suddenly reads as a named function in player-inventory.ts. The catch is that you must capture the exact build hash, because a source map from a different build will mis-resolve and quietly point you at the wrong line.
The browser events that actually fire
Two global events catch most browser crashes. The window error event fires for synchronous throws and uncaught exceptions during the frame loop, and it carries the message, filename, line, column, and crucially the error object with its stack. The unhandledrejection event fires when a promise rejects with no catch, which in a web game usually means a failed asset fetch, a rejected WebGL context request, or an await that nobody guarded. You need both listeners installed before your game loop starts.
Beyond the globals, wrap your requestAnimationFrame callback so a throw inside the update or render step is caught, reported, and does not silently kill the loop. A game that freezes on a black canvas with no console output is the worst kind of bug to receive secondhand. Capturing the throw, the frame number, and the elapsed time gives you a reproducible starting point instead of a vague report that the game just stopped.
Context that makes a web crash reproducible
Browser crashes are wildly environment dependent, so the report has to carry context. Record the user agent, the browser and version, the device pixel ratio, the canvas and WebGL renderer string, available memory if exposed, and whether the tab was backgrounded. A crash that only happens on a specific GPU driver, or only after the tab loses focus and the audio context suspends, is impossible to chase without that metadata attached to every single report.
Game state matters as much as platform. Capture the current scene, the active save slot, the random seed, and the last few player actions. For a web game you should also note the build hash and the asset version, because players keep stale tabs open for hours and a crash from yesterday's deploy will mislead you. The goal is that when you open a report you can recreate the exact frame, not reconstruct it from a one-line description.
Folding duplicate reports into one issue
A single broken code path on a popular browser can generate thousands of identical crashes in an hour. If each one is a separate ticket you will drown, and the genuinely rare bugs get buried under the noise. The answer is grouping by a fingerprint computed from the resolved stack trace, so every instance of the same underlying fault folds into one issue with an occurrence count. Now you see twelve thousand hits on one bug and forty on another, and you know exactly where to spend the afternoon.
Grouping also tells you the shape of an incident. A spike in occurrences right after a deploy points at a regression you just shipped, and the build hash on each report confirms it. A slow steady trickle across many browser versions points at something environmental. Counts turn a pile of anonymous errors into a signal you can read, and they let you confirm a fix actually stopped the bleeding rather than guessing from the absence of new complaints.
Setting it up with Bugnet
Bugnet gives a TypeScript web game both halves of this pipeline. The SDK installs the global error and unhandledrejection listeners and the animation-frame wrapper for you, so uncaught throws and rejected promises are captured the moment they happen, with the stack, the browser, the device context, and your captured game state already attached. You upload your source maps keyed to the build hash, and crashes are de-minified into real TypeScript file and line references in the dashboard rather than positions in bundle.js.
From there the in-game report button lets a player who hits a visual glitch rather than a hard crash send a structured report that snapshots the same scene, seed, and save context automatically. Occurrence grouping folds the duplicate crashes into single issues with live counts, so a post-deploy regression jumps to the top on its own. You filter by browser, build, or custom field, watch the count, ship the fix, and verify the curve flattens, all from one dashboard.
Make it part of the build, not an afterthought
Crash reporting for a web game works best when it is wired into your build pipeline rather than bolted on after launch. Make source map upload a step in your deploy script so every release is automatically resolvable, and stamp the build hash into the bundle so reports can be matched to the exact code that produced them. Test the whole loop before you ship by deliberately throwing an error in a staging build and confirming it arrives de-minified.
Treat the occurrence dashboard as a daily habit, not a fire alarm. A short look each morning catches the slow regressions that never generate angry messages, and watching counts fall after a deploy gives you real confidence the fix landed. A web game that captures, de-minifies, groups, and verifies its own crashes turns the browser from an opaque black box into a place where you can actually see what your players hit, and respond before the reviews do.
Minified traces are noise until you upload source maps. Capture the build hash, listen for both error and rejection events, and group by trace so counts do the prioritizing.