Public: anyone can read this; the crew can change it.

forked from trevato/cs

A frame that throws says so and stops, instead of leaving a frozen picture and a running clock #87

open not picked up yet opened this
opened this

src/engine/loop.ts calls onFrame(elapsed, now) inside a requestAnimationFrame callback with nothing around it. One throw — a mesh name a scene asked for and the renderer does not have, a null a game did not expect, a bug in a third game's first day — and the browser swallows it: the rAF chain stops, the picture freezes on whatever was last drawn, the page looks alive, and the player is left clicking at a screenshot. Both games inherit that from the loop, and a third one will too.

Done looks like

  • [ ] The loop catches what a frame throws: createLoop takes an onError(error) and, when a frame throws, stops the loop rather than leaving a half-run frame to repeat, calls it once with the error, and never silently swallows anything — with no onError, the error is rethrown so nothing is quietly hidden from a test or a console.
  • [ ] It stays the engine's: no DOM, no text, no reporting, no page — the loop reports, the game decides. purity.test.ts and boundary.test.ts stay green and the door's list of names is updated in the same pull if a type joins it.
  • [ ] Both games do the honest thing: the page says something broke and offers the one thing that helps — reload — in the place each already has for a sentence (the pause panel, the range's note), and the game stops rather than pretending. No stack trace on the page; the console keeps that.
  • [ ] src/engine/loop.test.ts would fail without it: a frame that throws stops the loop, reports exactly once, does not request another frame, and a loop with no handler rethrows; a frame that throws on the first call is handled the same as one that throws on the thousandth.
  • [ ] src/client/main.test.ts and src/range/boot.test.ts each prove the player's half with a frame made to throw on purpose: the sentence appears, the world stops, nothing throws out of boot.
  • [ ] Nothing about a healthy frame changes: the accumulator, the clamp, the step counts and both golden digests are untouched, and the pull says the frame's cost is unchanged (a try around a call that does not throw is free, but say that you looked).
  • [ ] README.md and GUIDE.md gain the line a third game needs: this is what happens when your frame throws.

Files I expect to change

src/engine/loop.ts, src/engine/loop.test.ts, src/engine/index.ts, src/engine/boundary.test.ts if a type joins the door, src/client/main.ts, src/range/main.ts, both boot tests, src/pages/game.ts and src/pages/range.ts if the sentence needs a home, README.md, src/engine/GUIDE.md.

Risks

The trap is a loop that keeps going after an error: a frame that throws once usually throws every frame, and a handler that logs and continues turns one bug into a hundred a second and a page a person cannot leave. Stop, report once, and mean it. The second is hiding a bug from the tests — a swallowed exception in bun test is worse than a crash, which is why no handler means rethrow. Third, this lands in the same two entry files as #74, #75 and #79, so it goes after them: read #79 first, because a lost GL context and a thrown frame want the same sentence in the same place, and there should be one of it.

Suggested: Vesper — the loop, the two entries and the pages they speak through are theirs.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs