← stitchkitDocumentation

ADR 0058 — Zod-first domain error definitions

  • Status: Accepted — extends the domain-free error boundary of ADR 0002, the published framework-code registry of ADR 0026, and cross-chunk branding from ADR 0032
  • Date: 2026-08-08

Context

A numeric CODE → status table can generate a thrower, but cannot describe the structured details allowed by each code. Positional message/details/hint arguments also erase that distinction and force construction and throwing into one action. Applications then duplicate the registry in a subclass, status map and runtime validator.

Decision

defineErrors accepts one immutable definition per application-owned code: { status, details? }. details is a required or optional Zod object. The definition derives all public artifacts:

  • a literal code table and guard;
  • a frozen definitions/status registry;
  • one options-object factory per code;
  • forbidden, required or optional details at compile time;
  • parsed structured details at runtime.

Factories construct branded generic AppError instances and never throw by themselves. The call site uses ordinary throw or passes/inspects the instance. HTTP and tool adapters keep their established envelopes; the factory does not choose serialization policy.

Alternatives rejected

  • Keep positional overloads. They preserve ambiguity and cannot express per-code details without a parallel type map.
  • Add defineErrorsV2. Two vocabularies would drift and leave every guide and consumer with a choice that has no semantic value.
  • Accept arbitrary details schemas. Error details are structured objects; scalars and arrays weaken diagnostics and envelope consistency.
  • Throw inside the generated function. Construction, composition and inspection remain impossible, and the helper behaves unlike an error class.

Consequences

  • One application registry replaces custom subclasses and copied status maps.
  • Invalid details fail at construction, before transport normalization.
  • Codes without schemas cannot silently acquire ad-hoc details.
  • The redesign is breaking: numeric definitions and positional throwers have no compatibility overload.