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:
- Single const object (
packages/schemas/src/errors.ts) mapping every error code to{ status, message, retryable }. TheErrorCodeNametype is derived from the keys, giving compile-time safety. - Comprehensive coverage — all existing error codes (~40) mapped in the initial version. No code is left as an ad-hoc string.
- Structured
detailonApplyResult(Record<string, unknown>) instead of raw strings, aligned withHttpError.detailsandErrorEnvelope.details. ErrorCodeNametyping onHttpError.codeandErrorEnvelope.error.code— any unregistered code fails at compile time.- Frontend derives everything from the registry —
errorMessage(),isRetryable(),isAuthError()read from the same source, never duplicating the code list. AuthErrorclass replaces rawthrow 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.
retryableflag enables auto-sync to make retry decisions from the error code alone.detailfield 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
- Per-handler error mappings (status quo ante) — rejected because it led to inconsistent messages and no discoverability of available codes.
- Separate backend/frontend registries — rejected because they would drift; sharing
@neemias/schemaswas already established. - 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 registryworkers/src/events/applyEventErrors.ts— applyEvent failure → HttpError helperapp/src/lib/errorMapping.ts— Frontend errormapapp/src/modules/auth/AuthError.ts— AuthError class