Gessa Docs
Product · Explanation

Concept

Explanation: Runtime Error Registry

Why runtime gateway failures carry a stable machine code from one closed catalog, how that catalog projects to both the frontend and the generated reference, and what the frontend adds on top.
engine v1.0.234since v1Copy for LLM
engine v1.0.232

Last verified 2026-09-03 against engine v1.0.232.

When a multiplayer session fails, a player should see one honest, player-safe screen and an operator should see one precise log line, and the two should never be confused for each other. Gessa gets there by making every runtime gateway failure carry a stable machine code from a single closed catalog, and by generating every downstream view of that catalog rather than hand-writing it. This page explains the model: where the taxonomy lives, how a thrown error becomes a public code, and which behaviors belong to the catalog versus the frontend. It frames the why. Every concrete code, its category, and its static copy live in the Runtime Error Catalog reference, which this page and the runtime error registry reference link down to and never restate.

The design authority is the netcode session-transport doctrine and its decision DN-06: errors are a closed enum, code-generated to both sides, and prose classification is deleted and lint-banned. The invariant the doctrine calls INV4 is the whole idea in one line: errors are typed, not parsed. A failure reaches the boundary already carrying a code; the boundary maps that code to copy from a table; nothing regexes another layer's message text.

One closed catalog

The taxonomy is a single object literal, RUNTIME_ERROR_CATALOG, in server/src/modules/runtime/errors/runtimeErrorCatalog.ts. It is typed as const satisfies Record<string, RuntimeErrorCatalogEntry>, which keeps every value literal (so the derived code union RuntimeGatewayErrorCode is exact) while still checking each entry against the entry shape. Each entry is plain, JSON-serializable data: a category, the booleans retryable and terminal, a playerTitle and playerMessage that are static and player-safe by construction, and an opsDescription that is log-facing and never reaches a player. Because the copy is fixed data rather than a formatted internal string, an internal detail cannot leak onto a player's screen.

Categories are their own closed tuple, RUNTIME_ERROR_CATEGORIES. Every code belongs to exactly one. The function runtimeErrorCategoryDisposition is a total switch over that tuple that ends in assertNever(category), so the moment a category is added without a matching case the file stops compiling. That compile-time totality is the structural guarantee behind the word "closed": the set cannot grow silently.

One family is matched by prefix rather than by an exact key. Engine-contract mismatches append a per-contract suffix to RUNTIME_ENGINE_CONTRACT_CODE_PREFIX, and the catalog carries a single canonical stem entry, runtime_engine_contract. The one lookup, runtimeErrorCatalogEntry, checks that prefix first, then exact ownership, and otherwise returns undefined; it is total over the closed union and undefined for anything else.

Throwing a code

A runtime throw site does not construct an ad hoc Error. It raises a RuntimeGatewayCodedError, whose constructor reads the catalog entry for its code and auto-populates code, category, retryable, and terminal, defaulting the human-readable message to the entry's opsDescription. The throw-site facade is codedError(code, cause). Because the coded error exposes a .code string, the existing error-reading helpers at the boundary keep working on it unchanged.

Two projections, one source

The catalog is the source of truth for two generated files, and this is the part worth stating precisely.

  • generateRegistrySource in scripts/generate-runtime-error-registry.mjs is the sole generator of the frontend presentation registry file, web_client/spa/src/runtime/generated/runtimeErrorRegistry.gen.ts. It renders entries deterministically in catalog order, mapping playerTitle to title and playerMessage to message and carrying category, retryable, and terminal.
  • gen-error-catalog-docs.ts reads the same catalog and emits the generated reference, docs/spec/generated/error-catalog.md.

So the frontend registry is not the catalog's only code-generated output; it is one of two projections. Both read the same catalog, which is exactly why the server's player copy and the frontend's player copy cannot drift apart: there is nothing to keep in sync by hand. A committed frontend registry that diverges from a fresh render fails the drift check, and the generated reference is held byte-exact the same way.

Classifying at the boundary

Not every error that reaches the gateway was thrown with a code. The boundary classifier runtimeGatewayPublicCode in server/src/modules/runtime/routes.ts is the one place that turns a raw thrown error into a stable public code, and it works entirely from typed values, never from prose:

  • Two internal codes have no catalog member of their own and alias onto one: capacity-exhausted becomes game_full, deployment-superseded becomes game_updating.
  • An authority-recovering error and a Postgres unique-violation are recognized by their typed error.code (the helpers isRuntimeAuthorityRecovering and isRuntimePersistenceConflict read the code, with SQLSTATE 23505 for the persistence conflict). They are not detected by inspecting message text.
  • Any error whose code is already a catalog member passes straight through.
  • Anything else is an untyped error that should have carried a code. It maps to runtime_internal, and the boundary logs loudly (through logRuntimeGatewayUntypedError) so the missing coded throw is visible rather than laundered into a vague transport failure.

Player copy at the boundary comes from runtimeGatewayPublicMessageForCode, which reads the catalog entry's playerMessage with a runtime_internal fallback. The hand-maintained public-message table that used to live in the routes file is gone; a conformance test scrapes the source to keep it gone.

What the frontend adds, and what it does not

The frontend is a projection of the catalog with a thin policy layer on top, and it is worth being explicit about which behaviors are the catalog's and which are the frontend's, because they are easy to confuse.

The catalog owns the player title, the player message, and the base retryable and terminal classification. The frontend module runtimeErrorPresentation.ts adds only presentation policy:

  • A finer presentation category, chosen by feCategoryForCode. This is a hand-maintained enum distinct from the taxonomy category, with a few phase overrides that are strict subsets of a taxonomy category (the first-frame family, already-open, lifecycle-continuable, authority-recovering). It falls back to a generic connection category for any taxonomy value it does not map, which means adding a taxonomy category does not automatically surface it here.
  • A Retry affordance, gated by a frontend list (NO_RETRY_ACTION). Whether a Retry button appears is a frontend decision, not catalog data.
  • A status-sensitive override, TERMINAL_WHEN_DISCONNECTED, for the first-frame snapshot and delta family. In the catalog these codes are retryable and non-terminal at the base; the frontend promotes them to terminal only once the transport is actually disconnected. That nuance is deliberately kept out of the catalog.

What this registry is not

Being honest about the edges matters more than a tidy story:

  • It is not the complete set of codes the gateway puts on the wire. runtime_origin_forbidden is a 403 emitted directly, outside the catalog and outside the classifier; it carries its own inline message and the frontend presentation returns nothing for it, falling back to generic copy. The catalog is the closed taxonomy the classifier produces, not every string the gateway can send.
  • It does not prove that each throw site picked the right code. The taxonomy gate proves every codedError("literal") names a catalog member; it does not prove the chosen code is the best classification for that failure.
  • One retained catch-all is still on its way out. runtime_connection_unverified is kept for server-table coverage during migration; new throw sites should use a specific split code instead (runtime_auth_invalid, runtime_credential_expired, runtime_release_stale_link, or runtime_session_ended).
  • It is not yet enrolled in the feature-contract registry, so the closure gate does not yet force its anchor to resolve to a docs page. It sits at the internal tier.

For the exact codes, their categories and dispositions, and the static copy each carries, read the generated Runtime Error Catalog reference. For the surfaces, invariants, and proof surfaces that back the claims on this page, read the runtime error registry reference.

Was this helpful?Report an issueContact support

On this page