Quick answer: Python games built on the Arcade library run on pyglet and OpenGL, so crashes arrive as Python tracebacks from uncaught exceptions, plus rarer faults in pyglet, the GL layer, or a frozen build. Hook sys.excepthook and threading.excepthook, wrap the game loop and event handlers, and attach the Python and Arcade versions, OS, and GPU so each traceback is fixable and duplicates group.

The Arcade library makes it pleasant to build a 2D game in Python on top of pyglet and OpenGL, and Python's tracebacks are a gift for debugging, naming the file, line, and exception type directly. The catch is that an uncaught exception in a distributed game closes the window and the traceback scrolls past in a console the player never sees, or in a frozen build, no console at all. Below your code sit pyglet and the GL layer, which can fault in ways a Python traceback never captures. This post covers how Arcade games fail and how to capture every traceback with the context to fix it.

How Arcade games crash

The everyday Arcade crash is an ordinary Python exception: an AttributeError on None, a KeyError in a dictionary, a TypeError from mixing types, or an IndexError on a list. These usually surface inside your Window subclass methods, on_update and on_draw and the input event handlers, since that is where most game logic runs each frame. Python hands you a full traceback with the file, line, and exception type, so once captured these are typically quick to fix because the traceback points almost exactly at the bug.

Below your Python code sit pyglet, which Arcade uses for windowing and the event loop, and OpenGL through pyglet's GL bindings. A failure here, a shader that will not compile on a player's GPU, a context that could not be created, or a fault in a native dependency, may raise a Python exception you can catch, or may crash deeper in the native GL layer where no traceback reaches. Distinguishing your logic exceptions from GL and pyglet trouble is the main triage you do.

Hooking Python exceptions

Python gives you sys.excepthook, a global hook that fires for any uncaught exception on the main thread, receiving the exception type, value, and traceback object. Override it at startup to capture the full formatted traceback, record your game state, and submit a report before the interpreter prints to a console no one will read. This single hook catches every unhandled exception that would otherwise close the game silently in a distributed build.

Threads need their own hook. Background threads, common for asset loading or networking in a game, do not route their exceptions through sys.excepthook, so also set threading.excepthook to capture exceptions raised off the main thread. Without it, a thread can die quietly and leave your game in a broken state with no report explaining why. Setting both hooks at startup ensures that no matter where an exception escapes, you capture the traceback that makes it fixable.

Wrapping the game loop and event handlers

Global hooks catch what escapes, but for an Arcade game you often want finer control inside the loop. Wrapping the bodies of on_update, on_draw, and your input handlers in try and except lets you report an exception with the precise method and the current game state, and decide whether to recover, skip a frame, or shut down gracefully, rather than letting one bad frame kill the window outright. This is especially useful for handlers that run on every player interaction.

Keep the wrapping purposeful so you do not swallow errors you would rather see during development. A common pattern is to report and re-raise in development builds, so you still get the immediate crash, but report and recover in release builds, so a single non-fatal exception does not end a player's session. Either way, attaching the specific handler name to the report turns a generic traceback into a clear statement of which part of the loop failed and under what game state.

Frozen builds and the context to attach

Most players receive an Arcade game as a frozen executable built with a tool like PyInstaller, not as a Python script, and freezing changes crash reporting in two ways. There is no console, so an uncaught traceback is invisible unless you captured it, which makes your excepthook essential rather than optional. And tracebacks may reference bundled paths, so keep your source mapping or build metadata to interpret them, and always record whether the report came from a frozen build or a development run.

On every report attach the Python version, the Arcade and pyglet versions, the OS and version, the GPU vendor and driver, and the OpenGL version and renderer string, since GL availability and shader behavior vary across player hardware. Add your game build version and current state. A traceback that names a GL call on one specific driver points at a rendering fallback, while a plain Python exception that hits everyone points at a logic bug, and the context is what lets you tell them apart at a glance instead of guessing.

Setting it up with Bugnet

Bugnet offers a Python-friendly SDK and an in-game report button that fit an Arcade game. Call the SDK from your sys.excepthook and threading.excepthook overrides and from your wrapped on_update and on_draw handlers, passing the formatted traceback, the handler name, and the Python, Arcade, GPU, and frozen-build context. Every crash, whether a logic exception or a GL failure, then lands in one dashboard already enriched, instead of scrolling past in a console no distributed player ever opens.

Occurrence grouping keeps a release calm. One common AttributeError on a hot code path can crash for many players at once, and Bugnet folds those identical tracebacks into a single counted issue, so a rarer GL or pyglet fault stays visible rather than buried. Custom fields like Python version, GPU driver, OpenGL version, and frozen-build flag become filters, so you can confirm a crash is isolated to one driver or to frozen builds and ship the exact fix rather than guessing from a pile of similar-looking tracebacks.

Testing the build players actually run

The most important testing lesson for an Arcade game is to test the frozen build, not just the script, because freezing introduces its own failures around bundled assets, paths, and missing modules that never appear when you run from source. Before release, build the frozen executable, deliberately raise an exception in a handler and on a background thread, and confirm both hooks capture and submit the report without a console present. Run it on a low-end GPU to exercise the GL paths real players will hit.

Once live, sort grouped tracebacks by occurrence, fix the loudest logic exceptions first, and watch counts fall while keeping an eye on the rarer GL and pyglet faults that point at hardware-specific problems. Python and Arcade give you fast iteration and readable tracebacks, and the gap to close is that distributed players never see those tracebacks. A reporting pipeline that captures every exception with version and GPU context closes that gap and keeps your game stable in the wild.

Arcade games run on pyglet and OpenGL, so set both excepthooks, wrap your loop handlers, and test the frozen build, since distributed players never see the console traceback.