Abstract
Problem: How should game programmers approach error handling β parameter checking, raw pointers, globals, exceptions, and when to reinvent the wheel vs. use libraries?
Approach: Tim Cain shares his decades of experience shipping games, covering where to place checks, what mechanisms to use, and how teams actually disagree on error handling strategies.
Findings: Check parameters in the receiving function (the bottleneck), use preprocessor directives to strip debug checks from release builds, and accept that globals and raw pointers have legitimate uses despite academic dogma. Teams will never fully agree on halt-vs-continue error strategies.
Key insight: Funnel your error checks to bottleneck locations β the function that actually cares about the parameter β rather than scattering checks throughout the codebase. This minimizes code bloat and makes breakpoint debugging far more effective.
Parameter Checking: Check at the Bottleneck
Tim checks parameters inside the receiving function, not the caller. The receiving function is the one that actually cares about the values. Many callers just pass parameters through without using them β they received a value only to forward it. Putting the check at the destination means one check in one place instead of scattered checks everywhere.
This also makes debugging easier: you set a breakpoint in the receiving function and walk up the call stack to find who sent the bad value.
Debug Checks and Release Builds
Tim uses asserts, if-then checks, and try-catch β but wraps them in preprocessor directives (#pragma or equivalent) so they're stripped from release builds. His reasoning:
- By the time the game ships, there's nothing the end user can do about a null pointer or out-of-bounds value
- Debug checks slow down release code for no benefit
- Console trace messages and developer-facing prints should also be wrapped and removed
The Crash Debate
Some programmers disagreed, arguing "but what if it crashes in release?" Tim's response: if a critical object is null, you often can't meaningfully continue anyway. A load function with an invalid file handle can't load anything β printing a message and stopping is all you can do.
Global Variables and Singletons Are Fine
Tim pushes back on the academic dogma that globals and singletons are always bad: "I wonder if any of them have really written code that was put into production." In game development, you genuinely need singleton classes β a save game manager, a creature object factory, a prop manager. There's no reason to have multiple instances of these.
Accessor Functions Over Raw Globals
Instead of exposing globals directly, Tim prefers get/set accessor functions. The key advantage is debugging: you can set a breakpoint on the setter to catch who's changing the value to null or out-of-range, rather than hunting through every place in the codebase that touches the variable.
The trade-off is speed β accessor calls have overhead. For hot loops, you may need to expose the raw global. But modern compilers will often inline single-line get/set methods anyway. Tim's advice: understand what your optimization flags actually do.
Raw Pointers vs. Handles
Sometimes you need raw pointers for speed. But Tim recommends handles as an alternative where possible. A handle is an indirect pointer β you "unlock" it to get the actual pointer, use it, then "lock" it again.
Benefits of Handles
- Debugging: Breakpoint on the unlock function to see who's using the pointer
- Memory defragmentation: If no handle pointing to a memory block is currently unlocked, you can safely move that block and the next unlock will resolve to the new location
The Handle Pitfall
Handles introduce a new class of bugs: code paths where a handle gets locked but never unlocked (e.g., an early return in a function). This leaves a memory block pinned in place for no reason.
Reinventing the Wheel vs. Using Libraries
Tim acknowledges the value of standard libraries (STL) and game engines β nobody wants to rewrite vectors and hash maps for every project. But he warns about the trade-offs:
- No source code: Some libraries don't ship source, making bug investigation painful. Is it your bug or theirs?
- Hidden inefficiency: On one project, a principal programmer measured the STL implementation and found "horrifically inefficient" implementations for basic data structures, specifically for their use patterns
- Time balance: Weigh the time to roll your own vs. the time you'll lose debugging someone else's code
Their team's resolution: use an STL with available source code, benchmark it, and warn programmers about which data structures had efficiency problems under their specific usage patterns.
The Great Halt-vs-Continue Debate
The most divisive error handling question Tim encountered: when a serious error occurs in development, should you halt the game or print a message and continue?
The Case for Halting
- Makes the bug immediately obvious and impossible to ignore
- Prevents cascading false bug reports in the tracker (e.g., "the quest monster wouldn't fight me" when the real bug was a spawn failure)
The Case for Continuing
- Lets other team members keep working β a spawn bug near a cave shouldn't block someone testing a quest down the road
- Not everyone's work depends on the broken system
Tim's Compromise
Early in development: halt on serious errors to get them fixed quickly. Later in development: switch to print-and-continue, with the understanding that any bug report related to an error message's subject is a known child issue and won't be investigated separately.
The result? Both camps were equally unhappy β the halters missed halting, the continuers wanted it from the start. "You will never make everybody happy."
References
- Tim Cain. YouTube video. https://www.youtube.com/watch?v=yhJEJKG-aK8