This repository has been archived on 2026-07-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kitchen-md/.claude/adr/0008-errors-as-values-at-cli-boundary.md
alexion ae542893da refactor: model bin errors as neverthrow Result values (task 0003)
Split the view command out of the entry point and make its failure a
value rather than an exception. index.ts is now scaffolding plus
dispatch; view.ts owns the behaviour, returning Result<string, ViewError>
(a plain-data tagged union) via neverthrow, with try/catch confined to a
readFile adapter. The entry point matches the Result at the boundary.

Because viewFile is a pure in-process function it is now unit-tested
directly (and counted by coverage), while the subprocess tests stay as
the end-to-end check. ADR 0008 records the errors-as-values convention.
2026-07-28 21:21:23 -04:00

2.0 KiB

ADR 0008 — Errors as Values at the CLI Boundary

Status: Accepted

Context

@kitchen-md/bin's view command performs a fallible operation: reading a file from disk. Node's readFileSync signals failure by throwing. Two idioms are available: let exceptions propagate and catch them at the entry point, or represent failure as a value the type system tracks. @kitchen-md/core's parser already takes the second path — it is a total function that never throws and reports problems through the Document AST's diagnostics (see ADR 0004).

Decision

Model fallible operations in @kitchen-md/bin as a neverthrow Result<T, E>, with error types expressed as plain-data tagged unions (e.g. ViewError = { tag: "read-failed"; … }). A throwing API is wrapped in a small adapter that converts the exception into an err, so nothing above the adapter leaks exceptions. The CLI entry point matches the Result at the boundary: stdout on success, stderr plus a non-zero exit on failure.

Rationale

It keeps the bin layer consistent with core's total-function stance, so the whole codebase treats recoverable failure as data rather than control flow. The possibility of failure becomes visible in a function's signature instead of hidden behind a throw. The success value cannot be read without first handling the error case, which removes a class of mistakes at compile time. A tagged-union error type gives exhaustive handling: a new failure mode is a new tag that every match must account for.

Consequences

@kitchen-md/bin takes a dependency on neverthrow. try/catch is confined to the thin adapters that wrap throwing APIs, and the rest of the layer is exception-free. Must-use enforcement — that a Result is never silently dropped — is a convention (always terminate a Result at a .match at the boundary), not a lint rule, because the project uses Biome alone and does not add ESLint's eslint-plugin-neverthrow for now. New failure modes stay additive: a new tag on the error union, handled at the boundary.