RFC-0012: Error taxonomy and exception hierarchy¶
- Status: Draft
- Author(s): GenoLeWM Project
- Created: 2026-05-20
- Updated: 2026-06-02
- Depends on: RFC-0001
- Supersedes: —
- Implementation status: Implemented for the v1 typed exception surface: stable error-code registry, typed subclasses, structured details/remediation payloads, and CLI exit-code mapping exist with unit/API coverage. Error-code docs generation and lookup helpers remain future work.
1. Summary¶
This RFC fixes the cross-cutting exception hierarchy used by every GenoLeWM subsystem, the error-code registry, the structured payload format, the CLI exit-code mapping, and the discipline around raising vs returning. The goal is a single mechanical contract for failure reporting so that callers (library, CLI, downstream tools) can route on type and code without reading docstrings.
2. Motivation¶
Without a shared error taxonomy, every subsystem invents its own
exceptions, the CLI's error messages become inconsistent, and downstream
tooling cannot distinguish "input malformed" from "model corrupt" from
"network refused." The taxonomy also gives the receipt format
(RFC-0011) a stable vocabulary
for documenting failure modes, and the observability layer
(RFC-0013) a stable code field for filtering.
3. Specification¶
3.1 Hierarchy¶
The full hierarchy is in docs/spec/04-error-model.md.
The root is geno_lewm.errors.GenoLeWMError. Top-level branches:
ConfigErrorInputErrorResourceErrorTrainingErrorEvalErrorDeployErrorProvenanceErrorInternalError
Each branch has named leaves enumerated in the spec document. Adding a leaf is a MINOR change; renaming or removing one is a MAJOR change.
3.2 Payload¶
class GenoLeWMError(Exception):
code: str
message: str
details: dict[str, object]
remediation: str | None
def __init__(self,
message: str,
*,
code: str,
details: dict[str, object] | None = None,
remediation: str | None = None) -> None: ...
def to_dict(self) -> dict[str, object]: ...
def to_json(self) -> str: ...
code strings are dotted-uppercase, category-prefixed, and registered in
geno_lewm/errors.py::ERROR_CODES. Examples in
docs/spec/04-error-model.md.
3.3 Registry¶
ERROR_CODES is a tuple of (code, exception_class, summary) tuples.
The linter rule ast/check_error_codes.py walks every raise statement
in geno_lewm/ and confirms:
- The raised expression is a
GenoLeWMErrorsubclass. - The
codekeyword argument is a literal string registered inERROR_CODES. - The exception class matches the second element of the registered tuple.
Violations fail CI.
3.4 CLI exit codes¶
| Code | Category |
|---|---|
| 0 | success |
| 2 | InputError |
| 3 | ConfigError |
| 4 | ResourceError |
| 5 | TrainingError |
| 6 | EvalError |
| 7 | DeployError |
| 8 | ProvenanceError |
| 9 | InternalError |
| 1, 130 | reserved |
CLI dispatcher: geno_lewm/cli/_dispatch.py::main catches GenoLeWMError
at exactly one place, prints a one-line summary plus the structured
payload, and exits with the mapped code.
3.5 Raise discipline¶
- Validation failure of caller-supplied input: raise typed
InputErrorsubclass with acodethat names the rule. - Expected absence (cache miss, optional field): return
Noneor a sentinel; document in the API. Never raise. - Resource exhaustion: raise typed
ResourceErrorsubclass with adetailspayload naming the resource and the budget exceeded. - Internal invariant violation: raise
InvariantViolation; log at ERROR; never silent.
3.6 Translation to logs and receipts¶
Every raised exception inside a logged span emits an
{event: "error", code, message, details, remediation, ts} log record.
The receipt format does not include errors; partial-failure scoring (one
variant of many) records per-variant errors in an adjacent
.errors.jsonl file.
3.7 Module shape¶
A single file geno_lewm/errors.py defines every class and the registry.
No submodules. This is intentional: callers can do from geno_lewm.errors
import * and trust the full surface is visible.
4. Rationale and alternatives¶
4.1 Why a single file?¶
Splitting the hierarchy into per-subsystem modules would force callers to know which subsystem raised what before importing the right module — exactly the discovery problem the taxonomy is meant to solve. A single file is the simplest contract.
4.2 Why stringly-named codes rather than enums?¶
Codes are part of the stable external contract and need to be greppable across logs, receipts, and CHANGELOGs. Enums add a Python layer that serializes back to the same string anyway; the simpler approach is just to use the string and lint that it is registered.
4.3 Why not adopt a third-party error library?¶
pydantic.ValidationError, attrs.exceptions, tenacity retry errors
— each addresses a slice but none cover the cross-cutting hierarchy we
need. A bespoke 200-line errors.py is cheaper than aligning with one
of those libraries.
4.4 Why typed exceptions over Result[T, E]?¶
Python's exception ergonomics are the language's preferred style, and the
public surface is more readable with raise InvalidEditError(...) than
with return Err(InvalidEdit(...)). We considered a Result-style API
(OQ-ERR-2) for future
versions; v1 ships typed exceptions.
5. Unresolved questions¶
- Whether to expose typed
Result[T, E]returns at API boundaries in a later major version. Tracked as OQ-ERR-2. - Whether to integrate
sentry-sdkor similar for upload of crash diagnostics. Only acceptable if the redaction filter is enforced; see OQ-ERR-3. - Whether to provide
__cause__-chain helpers for downstream diagnostics. Tracked as OQ-ERR-1.
6. Future work¶
- Auto-generated docs from
ERROR_CODESintodocs/api/error-codes.md. - A
geno-lewm errors lookup CODECLI for fast diagnostic lookup. - Programmatic remediation: each leaf may expose a
try_fix()method that attempts a documented recovery (e.g.,CacheCorruptError.try_fix()callscache-windows --repair).
7. Changelog¶
- 2026-06-02 — Updated implementation status for the v1 typed error hierarchy and CLI exit-code mapping.
- 2026-05-20 — Initial draft.