Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-0009 — Error taxonomy by fault, stable exit codes, miette at the edge

Status: Accepted · Date: 2026-07-28

Context

The error model categorizes by who is at fault — User / TestFailure / System(anyhow) — with a total mapping to its stable exit-code scheme (0 ok · 1 test failure · 2 user error · 3 system error), integration-tested via assert_cmd. 2026 ecosystem consensus (blessed.rs et al.): thiserror in libraries, anyhow at the application edge; miette’s Diagnostic adds codes/help/labeled source spans for user-facing errors. Survey of backend traits (tower/sqlx/rustls/probe-rs): behind dyn, a unified error enum with boxed sources beats associated type Error. gherkin 0.16 spans are byte offsets (verified), directly convertible to miette SourceSpan (with an EOF-trailing-newline clamp; LineCol.column is char-counted — never mixed into byte math). snafu/error-stack: adopted by some large codebases, unnecessary at this crate count.

Decision

A fault-category model, extended for engines: proef-core defines

#![allow(unused)]
fn main() {
pub enum CoreError { User(..), TestFailure(..), System(..) }        // → exit 2 / 1 / 3
pub struct EngineError { pub class: EngineErrorClass,               // Infra | AssertFailed | Setup
                         pub message: String,
                         pub source: Option<Box<dyn Error + Send + Sync>> }
}

AssertFailed folds into TestFailure; Infra/Setup into System. thiserror 2 everywhere; no anyhow in library crates (only inside System’s boxed source at the edge). miette lives only in proef-cli: parse/bind/validation errors wrap into Diagnostics with labeled spans into .feature files (gherkin byte spans) and pack YAML (serde_norway locations); engine failures render the feature line + artifact span from the sidecar. Exit codes are a typed enum, pinned by CLI integration tests.

Consequences

Every failure has a fault category, a stable exit code, and a source-located rendering; engine crates stay miette-free (usable headless); the explain command reuses the same classification. Cost: two error layers (core vs engine) — justified by the seam: engines can’t know exit codes, core can’t know engine internals.

Alternatives considered

Associated type Error on engine traits — erased behind dyn anyway. anyhow everywhere — loses matchable categories that exit codes require. snafu — per-crate context ergonomics we don’t yet need; revisit if the workspace grows past ~10 crates.

Amendment — 2026-08-04 (exit code 130 documented, not a variant)

test and watch hard-exit with code 130 (128+SIGINT) on a second Ctrl-C while a run is already cancelling (ADR-0007) — the shell’s own convention for a signal-terminated process. This is a sanctioned OS-signal escape hatch, not a graceful outcome the fault-category model classifies, so it is intentionally not an ExitCode variant; ExitCode stays the total 0/1/2/3 mapping above.

Amendment — 2026-09-06 (every signal, and undelivered output)

Ctrl-C, SIGTERM and SIGHUP all take the graceful cancel (ctrlc’s termination feature), so a CI job timeout or docker stop is a cancelled run — exit 1 with a complete record — not a kill; the 130 hard exit above fires on a second signal of any of the three, and the handler carries no signal identity, so one code covers them all. And output proef could not deliver never looks like success: a failed write of the run record, of a JUnit or CTRF file, or of the GitHub step summary re-classifies the exit to System (3) through one fold, escalate_environment_failures, beside the stdout latch this taxonomy already covered.