Skip to content

ADR-0024: Unified Error Handling — Centralized ErrorCode Registry ​

Context ​

Error handling across the codebase was inconsistent. Every layer had its own pattern:

  • API handlers threw generic "Failed to create/update/delete X" with fallback codes like "CREATE_FAILED" — the caller could not distinguish a projection misconfiguration from a D1 constraint violation.
  • Error codes were ad-hoc strings ("BAD_REQUEST", "UNAVAILABLE") with no registry or documentation. There was no way to know which codes existed or what they meant.
  • Frontend had no centralized way to map error codes to user-facing messages — components hardcoded string checks like err.message === "AUTH_BACKEND_UNAVAILABLE".

Meanwhile, a functioning ErrorEnvelope type and createHandler middleware already existed, producing the correct shape { error: { code, message, correlationId, details? } }. The gap was in the content, not the container.

Decision ​

We established a single centralized ErrorCode registry in @neemias/schemas:

  1. Single const object (packages/schemas/src/errors.ts) mapping every error code to { status, message, retryable }. The ErrorCodeName type is derived from the keys, giving compile-time safety.
  2. Comprehensive coverage — all existing error codes (~40) mapped in the initial version. No code is left as an ad-hoc string.
  3. Structured detail on ApplyResult (Record<string, unknown>) instead of raw strings, aligned with HttpError.details and ErrorEnvelope.details.
  4. ErrorCodeName typing on HttpError.code and ErrorEnvelope.error.code — any unregistered code fails at compile time.
  5. Frontend derives everything from the registry — errorMessage(), isRetryable(), isAuthError() read from the same source, never duplicating the code list.
  6. AuthError class replaces raw throw new Error("string") in frontend auth services, carrying a machine-readable code.

Consequences ​

Positive:

  • Single source of truth for all error codes — no more ad-hoc strings.
  • Compile-time safety — unregistered codes are rejected by TypeScript.
  • Frontend and backend share the same code registry.
  • retryable flag enables auto-sync to make retry decisions from the error code alone.
  • detail field provides structured context for debugging and user-facing messages.

Negative:

  • Adding a new error code requires updating the registry — a small but real overhead.
  • The registry is updated at build time; runtime changes require a deploy.

Alternatives Considered ​

  1. Per-handler error mappings (status quo ante) — rejected because it led to inconsistent messages and no discoverability of available codes.
  2. Separate backend/frontend registries — rejected because they would drift; sharing @neemias/schemas was already established.
  3. Weak typing (keep code: string) — rejected because compile-time safety catches typos and unregistered codes before CI.

References ​

  • PRD #389 — Unified error handling
  • Issues #387, #388 (superseded by #389)
  • packages/schemas/src/errors.ts — ErrorCode registry
  • workers/src/events/applyEventErrors.ts — applyEvent failure → HttpError helper
  • app/src/lib/errorMapping.ts — Frontend errormap
  • app/src/modules/auth/AuthError.ts — AuthError class

Distribuído sob licença MIT.