---
title: "Explanation: Runtime Error Registry"
description: "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."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/explanation/runtime-error-registry/
---

# Explanation: Runtime Error Registry

{% version engineVersion="v1.0.232" coordinate="docs-public.v0" /%}

_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](../../../spec/generated/error-catalog.md), which this page and the [runtime error registry reference](../reference/runtime-error-registry.md) 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](../../../spec/generated/error-catalog.md). For the surfaces, invariants, and proof surfaces that back the claims on this page, read the [runtime error registry reference](../reference/runtime-error-registry.md).
