HTML5 game publishing

Error Handling in HTML5 Games: No More Frozen Screens

Global handlers, a guarded game loop, a friendly error screen, graceful fallbacks and error logs without third-party trackers.

PlayKovo Team4 min read
Games currently available on PlayKovo. Open a title to see its current listing.

Every game has bugs that only show up on someone else's device. When an error happens in a browser game, the usual result is a frozen screen or a black canvas, and the player simply leaves. You never hear about it. This post covers how to catch errors in an HTML5 game, show players something useful instead of a frozen screen, and collect enough information to fix the problem, without adding third-party tracking that portals may reject.

Catch what you can

Two global handlers catch most uncaught errors in the browser:

window.addEventListener('error', e => {
  report({ type: 'error', message: e.message, source: e.filename,
           line: e.lineno, col: e.colno, stack: e.error?.stack });
});

window.addEventListener('unhandledrejection', e => {
  report({ type: 'promise', message: String(e.reason?.message || e.reason),
           stack: e.reason?.stack });
});

The first catches thrown errors. The second catches rejected promises nobody handled, which is common with asset loading and async code. Errors in resource loading (an image or script that fails) do not bubble to window as normal errors, so also listen for error events on the elements or loaders you use.

Errors from scripts on another origin only show as "Script error." with no details, unless those scripts are served with CORS headers and loaded with crossorigin. Keeping your game's scripts on the same origin as the game avoids that.

Wrap the game loop

An error inside requestAnimationFrame stops that frame, but the next frame is never requested, so the game freezes. Catch errors in the loop itself:

function frame(now) {
  try {
    update(now);
    render();
  } catch (err) {
    report({ type: 'loop', message: err.message, stack: err.stack });
    errorCount++;
    if (errorCount > 3) return showErrorScreen();
  }
  requestAnimationFrame(frame);
}

Allowing a few errors before giving up lets the game survive a single bad frame (for example, one broken particle) while still stopping a game that is completely broken.

Show a friendly error screen

A frozen canvas looks like the game is loading or the device is too slow. A clear message is much better:

  • A short sentence: "Something went wrong."
  • A Restart button that reloads the game.
  • If saves exist, reassure the player that progress is saved, but only if that is true.
  • An error code or short ID the player can mention if they contact support.

Draw this in HTML on top of the canvas, not on the canvas itself. If the renderer is what broke, canvas drawing may not work either.

Fail gracefully on missing features

Some errors are predictable: WebGL not available, storage blocked, audio failing to start. Handle them where they happen instead of letting them crash the game:

  • WebGL: check that a context could be created. If not, show a message that the browser or device does not support the game, rather than a black screen.
  • Storage: wrap localStorage in try/catch. If it fails, run without saving and keep the session going.
  • Audio: if a sound fails to decode, skip it.
  • WebGL context loss: on phones, the GPU context can be lost when the app goes to the background. Listen for webglcontextlost and webglcontextrestored, and either rebuild resources or show the restart screen.

Collecting reports without third-party trackers

Many portals, PlayKovo included, reject games that send data to third-party analytics or trackers. That rules out dropping in a hosted error-tracking script without checking the portal's rules first. Options that stay inside the rules:

  • Keep a local error log in memory (the last 20 errors) and in localStorage. Show it in a hidden debug view you can open during testing, or let support ask players to read the error code from the error screen.
  • Use the platform's own tools. Check what the portal's SDK and dashboard provide. PlayKovo's documentation describes how to report SDK problems to support.
  • Collect reports in your own testing. Test on many devices yourself or with testers, with the debug view open.

Before adding any network call to a game you submit, read the portal's rules on network requests. The PlayKovo submission rules explain what is and is not allowed.

What to record with each error

  • The message and stack trace.
  • The game version or build number.
  • Where in the game it happened: scene, level ID, last action.
  • Basic environment: user agent, screen size, whether WebGL 2 is available.
  • A timestamp.

Do not record personal information. Names, emails or anything typed by the player should never go into error logs.

Make stack traces readable

Minified code produces stack traces like a.b (index-3f2a.js:1:20431). Build source maps, but do not necessarily ship them publicly. Keep them with each release build so you can map a stack trace back to your source when you investigate. Tag every build with a version number, so you know which source map matches.

A stack trace tells you where the error happened, not how the player got there. Keep a short list of recent events in memory, called breadcrumbs:

const crumbs = [];
function crumb(msg) {
  crumbs.push(`${Math.round(performance.now())} ${msg}`);
  if (crumbs.length > 30) crumbs.shift();
}
crumb('level start forest-07');
crumb('used bomb');

Include them in the error log. "Level started, bomb used, level restarted, error" is often enough to reproduce the bug.

Test your error handling

  • Throw an error on purpose inside the loop: does the error screen appear?
  • Block an asset in DevTools: does the game still start or show a clear message?
  • Disable storage in the browser: does the game keep running?
  • Force a WebGL context loss with the WEBGL_lose_context extension: what happens?

Checklist

  • Global handlers for errors and unhandled rejections.
  • The game loop wrapped, with a limit before giving up.
  • An HTML error screen with a restart button.
  • Graceful handling for WebGL, storage, audio and context loss.
  • No third-party trackers unless the portal allows them.
  • Version, scene and breadcrumbs in each error log, no personal data.
  • Source maps kept for each build.

Read the submission rules before adding any network requests, and see the developer blog for more guides.

We use optional cookies for site analytics and site advertising. You can change your choice later. Privacy policy.