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 registryerrorMessage(), 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.