← All dev notes

One error stopped my game loop for good

12 October 2026

I've found two bugs in my game that froze it solid, with no error on screen and no way back except reloading the page. Both came down to one line of drawing code throwing an exception. The first could freeze the game at the end of the first boss fight. The second would only ever have hit people who had paid me money.

If you write a browser game with a hand-rolled requestAnimationFrame loop, you can have this bug right now and not know it.

Why one error stops everything

The usual game loop looks like this:

function loop(){
  update();
  draw();
  requestAnimationFrame(loop);
}

The loop only keeps going because each frame asks for the next one on its last line. If anything above that line throws, the function exits early and the next frame is never requested. Nothing restarts it. The canvas keeps showing the last thing it drew, so it looks like the game hung, but the page is actually fine. The game just stopped asking to run.

The browser logs the error to the console, which players never open. From their side the game simply stops.

Bug one: the boss fight you couldn't finish

Stage 10 is the first boss, and it could lock the game up for good right as you were winning. That's about the most annoying place a game can freeze.

The boss has a health bar drawn with a canvas gradient. The red part ends wherever the boss's health is, so the code worked out the boss's remaining health as a fraction and used it as a gradient stop:

const pct = boss.hp / boss.maxHp;
grad.addColorStop(pct, 'rgba(0,0,0,0)');

The Ki special is a big screen-clearing attack that does a fixed 580 damage at the starting upgrade level. It's the obvious thing to save for finishing a boss. If the boss had 100 health left and took 580, its health went to minus 480, and pct came out negative.

addColorStop only accepts offsets from 0 to 1. Pass it anything else and it doesn't clamp or ignore it. It throws an IndexSizeError. The throw escaped the loop, no next frame was requested, and the game froze with the boss beaten and the win screen never coming.

Bug two: the freeze only paying customers would get

This one I'm less proud of. The game sells a 99 cent starter bundle that includes a bronze sword skin. When the game loaded for someone who owned it, it set their style to 'bronze'.

Skins were defined in two tables, one listing the skins you can own and one holding the colours the renderer uses for the kimono and blade. Both are looked up with the same key. Bronze had been added to the first table and never to the second. So every frame, the renderer did this:

const s = STYLES[styleKey];   // undefined for 'bronze'
ctx.fillStyle = s.blade;      // TypeError

That's the same failure as the boss bar. A throw inside the draw call, no next frame, a frozen screen. Free players could never see it because they never had the bronze skin. Every single person who bought the bundle would have. Nobody had bought it yet when I found it, which is the only reason this is a funny story and not a refund queue.

The fix, in two layers

The first layer is fixing the actual bugs. The health fraction is clamped before it goes anywhere near the gradient:

const pct = Math.max(0, Math.min(1, boss.hp / boss.maxHp));

And the bronze colours went into the table they were missing from. I added the one other skin that had the same gap while I was there, even though nothing can select it yet.

The second layer is the one I'd recommend to anyone. Make the loop survive its own errors:

function loop(ts){
  try { draw(); }   catch (err) { console.error(err); }
  try { update(); } catch (err) { console.error(err); }
  requestAnimationFrame(loop);
}

Now a bad frame is one bad frame. The error still gets logged so you can find it, but the next frame still gets requested and the game keeps running. A glitched health bar for one frame is a bug report. A frozen game is a player who closes the tab and doesn't come back.

Putting draw and update in separate try blocks matters. If they share one, a draw error also skips the update, so the simulation silently stalls even though frames keep coming.

Is this hiding bugs?

A bit, and I went back and forth on it. The argument against is that swallowing errors lets broken code keep running in a broken state. In a game I think that's the right trade. The alternative isn't the player seeing a helpful error, it's the player staring at a still image until they give up. And the errors still go to the console.

What I'd change is that I should have been watching for them. If you have any kind of analytics, send a count of caught loop errors along with it. A sudden jump in that number is a bug report that arrives before anyone has to write one.

Check your own game

Search your code for addColorStop, createRadialGradient and arc. Wherever the argument is calculated from game state, especially health or progress, ask what happens when that value goes negative or past its maximum. Then wrap the loop body. It takes two minutes, and it's the difference between a glitch and a game that just stops.

Play Samurai Vengeance