16 October 2026 · 7 min read
Every bug in this list was invisible in normal play and would have shipped if it had not been caught by a test or by luck. They are written down because each one points at a mistake that is easy to repeat.
The symptom was a completely blank screen. No error, no crash, just nothing.
The cause was a scale transform applied every frame and never reset. With a device pixel ratio of 3, the first frame drew at 3x, the second at 9x, the third at 27x, and within a dozen frames the whole scene was drawn a billion pixels off screen.
The fix was one line in the right place: save the context before scaling, restore it after drawing. The lesson is that a graphics context is stateful, and any state you set you must also unset.
The most serious bug in the project.
A button that should have granted an ad-free upgrade after a successful payment was instead granting it unconditionally. The payment path was stubbed out during development, and the stub returned success.
If that had shipped, anyone could have tapped the button and received the paid upgrade for nothing. It was caught by reading the code carefully rather than by testing, because testing the happy path passes — the happy path is what the stub fakes.
The fix was a hard gate: the feature is disabled by default, and only enabled by a flag that is set once the store product exists and has been tested with a real transaction. The button is hidden while the flag is false.
The lesson is uncomfortable but clear: a payment path that has never taken a real payment is not tested, no matter how green the tests are.
A player with one life left could miss candy indefinitely without the game ending.
An early return in the miss handler skipped the game-over check when the remaining life count was already low. The intent was to avoid a double-trigger. The effect was that the last life was never lost, so the game continued forever with a score that kept climbing.
The fix was to remove the guard and let the check run, then guard against the double-trigger by state rather than by an early return.
The lesson: guards that skip a whole function are dangerous. Guard the specific action instead.
A player reported the game freezing on a particular round — audio continuing, candy not falling, level not increasing.
The cause was not a freeze at all. The round-complete overlay was showing, but tapping the button did nothing, so from the player's side it looked like a frozen game.
The real root cause turned out to be a stale service worker serving a build from several versions earlier, where the button genuinely had been broken. The bug had already been fixed and the player simply never received the fix.
Two lessons, and the second is the important one. First, "the game froze" and "the game is waiting for input it cannot receive" look identical. Second, if your caching strategy can serve old code, you will get bug reports about bugs that no longer exist, and you will have no way to reproduce them.
Leaving the app in the background and returning to it produced a large jump in the animation — candy teleported down the screen.
The cause was time-based animation using the raw delta since the last frame. Coming back from the background after thirty seconds meant one frame with a thirty-second delta, and everything moved thirty seconds' worth in a single step.
The fix was to clamp the delta to a maximum. A frame never advances the world by more than about two normal frames' worth of time, no matter how long the gap was.
The lesson: time-based animation is correct in principle and unsafe in practice unless you clamp the time step.
Four of the five were invisible in normal play. They needed either a specific device state, a specific timing, or a code review to find.
That is the argument for tests that simulate many states rather than trying the game by hand, and for treating anything involving money as untested until real money has moved through it.
The one that got away was found from a player report, which is a reminder that the last place bugs hide is in the gap between what the code does and what the player sees.