# ADR-001: Errors as Values, Not Exceptions **Date**: 2026-03-25 **Status**: accepted ## Context Domain functions need to communicate failure (invalid input, missing combatant, violated invariants). The standard JavaScript approach is to throw exceptions, but thrown exceptions are invisible to TypeScript's type system — nothing in a function's signature tells the caller that it can fail or what errors to expect. This project's domain layer is designed to be pure and deterministic. Thrown exceptions break both properties: they alter control flow (a side effect) and make the function's output unpredictable from the caller's perspective. ## Decision All domain functions return `SuccessType | DomainError` unions. `DomainError` is a plain data object with a `kind` discriminant, a machine-readable `code`, and a human-readable `message`: ```typescript interface DomainError { readonly kind: "domain-error"; readonly code: string; readonly message: string; } ``` Callers check results with the `isDomainError()` type guard before accessing success data. Errors are never thrown in the domain layer (adapter-layer code may throw for programmer errors like missing providers). ## Alternatives Considered **Thrown exceptions** — the JavaScript default. Simpler to write (`throw new Error(...)`) but error paths are invisible to the type system. The caller has no compile-time indication that a function can fail, and `catch` blocks lose type information about which errors are possible. Would also make domain functions impure. **Result wrapper types** (e.g., `neverthrow`, `ts-results`) — formalizes the pattern with `.map()`, `.unwrap()`, `.match()` methods. More ergonomic for chaining operations, but adds a library dependency and a layer of indirection. The project's use cases are simple enough (call domain function, check error, save or return) that raw unions are sufficient. **Validation libraries** (Zod, io-ts) — useful for input parsing but don't cover domain logic errors like "combatant not found" or "no previous turn". Would only address a subset of the problem. ## Consequences **Positive:** - Error handling is compiler-enforced. Forgetting to check for an error produces a type error when accessing success fields. - Domain functions remain pure — they return data, never alter control flow. - Error codes are stable, machine-readable identifiers that UI code can match on. - Testing is straightforward: assert the return value, no try/catch in tests. **Negative:** - Every call site must check `isDomainError()` before proceeding. This is slightly more verbose than a try/catch that wraps multiple calls. - Composing multiple fallible operations requires manual chaining (check error, then call next function). A Result wrapper would make this more ergonomic if the codebase grows significantly. - Contributors familiar with JavaScript conventions may initially find the pattern unfamiliar.