Per-Crate thiserror Error Enums, No Shared Top-Level Error Type
ADR-0005: Per-Crate thiserror Error Enums, No Shared Top-Level Error Type
Section titled “ADR-0005: Per-Crate thiserror Error Enums, No Shared Top-Level Error Type”Status
Section titled “Status”Accepted
Context
Section titled “Context”Background and Problem Statement
Section titled “Background and Problem Statement”mif-rs’s library crates each fail in genuinely different ways: mif-schema
fails on JSON Schema validation, mif-ontology fails on corpus/graph
resolution (cyclic extends chains, missing namespaces), mif-frontmatter
fails on markdown/JSON-LD projection, mif-embed fails on model loading, and
mif-store fails on SQLite operations. None of these failure spaces overlap
in any way that a single shared error type could represent without either
flattening every crate’s variants into a lowest-common-denominator shape or
boxing them behind a dynamic dyn Error.
At the same time, every one of these failures must also render as a
consistent [RFC 9457] application/problem+json envelope, since mif-cli
and mif-mcp now answer to two audiences — the human reading the terminal
and the LLM agent parsing structured output to decide whether to retry,
escalate, or abandon. That envelope shape needs to be uniform across all
seven error-producing crates in the workspace (mif-schema, mif-ontology,
mif-frontmatter, mif-embed, mif-store, mif-cli, mif-mcp) without
forcing them into one shared error hierarchy to get there.
Current Limitations
Section titled “Current Limitations”- No prior workspace-wide error convention: absent a decision, each new
crate could independently choose
thiserror,anyhow,eyre, or ad hocStringerrors, leaving callers with no consistent way to match on failure kinds across crates. - The RFC 9457 envelope needs one shared shape, not one shared type: the
agent-facing
application/problem+jsonoutput (type,title,status,detail,instance, plus theretry_after/suggested_fix/code_actionsextensions) must look identical regardless of which crate raised the error, but the underlying Rust error values are not interchangeable. - Matchability matters to callers: a caller distinguishing a schema-validation failure from a store I/O failure needs variant-level pattern matching, which a fully erased/dynamic error type would foreclose.
Decision Drivers
Section titled “Decision Drivers”Primary Decision Drivers
Section titled “Primary Decision Drivers”- Scoped failure modes: each crate’s error enum shall stay scoped to that crate’s own actual failure modes, not a shared vocabulary that fits no single crate well.
- Zero-overhead derivation: error
Display/std::error::Errorimplementation shall not add runtime overhead or require hand-written boilerplate per variant. - Uniform machine-readable envelope: every crate’s errors shall render
as the same RFC 9457
application/problem+jsonshape for agent consumers, without requiring the crates to share an error type to get there.
Secondary Decision Drivers
Section titled “Secondary Decision Drivers”- Variant-level matchability: callers (human code or an agent) shall be able to match on a specific error variant (e.g. a cyclic-ontology error) rather than only a generic failure.
Considered Options
Section titled “Considered Options”Option 1: One shared top-level mif-rs::Error enum
Section titled “Option 1: One shared top-level mif-rs::Error enum”Description: Define a single top-level Error enum that every crate’s
fallible functions return or convert into.
Advantages:
- One error type to learn across the whole workspace.
Disadvantages:
- Forces every library crate to either depend on a shared error crate — real
coupling across the entire dependency graph, in a workspace that otherwise
keeps crates independently scoped — or box/erase into
anyhow-style dynamic errors, losing variant-level matchability for callers who need to distinguish, say, a schema-validation failure from a store I/O failure. - A shared enum’s variants inevitably become either too generic to be useful per-crate, or bloated with every crate’s specific failure modes mixed together.
Risk Assessment:
- Technical Risk: Medium. A shared error type becomes a coupling point every crate must agree on before it can ship a new failure mode.
- Schedule Risk: Medium. Adding a new crate to the workspace means negotiating changes to a shared type other crates also depend on.
- Ecosystem Risk: High. Undermines the workspace’s existing pattern of independently scoped library crates (see ADR-0004).
Option 2: Each crate owns its own thiserror-derived enum (chosen)
Section titled “Option 2: Each crate owns its own thiserror-derived enum (chosen)”Description: Each crate defines its own thiserror-derived error enum
(MifSchemaError, OntologyError, FrontmatterError, EmbedError,
StoreError, plus the two binaries’ own CliError/McpError), and every one
of these enums implements the shared mif_problem::ToProblem trait for the
RFC 9457 envelope, using mif_problem::ProblemMeta (slug, version, title,
status, exit code) to keep each crate’s own URI/status/exit-code bookkeeping
in one place.
Advantages:
- Each error enum stays scoped to that crate’s own actual failure modes —
MifSchemaErrordescribes schema-validation failures,OntologyErrordescribes corpus/graph-resolution failures, and so on, with no forced overlap. thiserrorgives zero-runtime-overheadDisplay/Errorderivation from attributes, with no hand-written boilerplate per variant.mif_problem::ToProblemgives every crate the same RFC 9457 envelope shape without requiring them to share an error type — the envelope is uniform, the underlying error values are not.- Callers can exhaustively match a specific crate’s own variants.
Disadvantages:
mif-cli’s andmif-mcp’s own error enums must explicitly wrap each library error type via#[from]variants rather than getting a shared conversion for free from a common base error type.
Risk Assessment:
- Technical Risk: Low.
thiserroris already the workspace’s existing error-derivation convention (see this repo’sCLAUDE.md, “Whythiserrorfor Errors”). - Schedule Risk: Low. Adding a new crate’s error enum is independent work that does not require negotiating a shared type.
- Ecosystem Risk: Low. Matches the workspace’s existing pattern of independently scoped library crates.
Option 3: anyhow/eyre dynamic errors everywhere, no typed enums
Section titled “Option 3: anyhow/eyre dynamic errors everywhere, no typed enums”Description: Every fallible function returns anyhow::Error (or eyre’s
equivalent) instead of a typed enum, with no per-variant structure at all.
Advantages:
- Fastest option to adopt — no per-variant enum to define, no
mif_problem::ProblemMetabookkeeping to author per failure mode. - No enum to keep exhaustively up to date as new failure modes appear;
anyhow::Error/eyre::Reportaccept anystd::error::Errorwithout a matching arm.
Disadvantages: Loses the ability for a caller — or an agent parsing
structured output — to match on a specific error variant, which undermines
the entire reason mif-problem exists: a structured, machine-readable error
surface. A dynamic error’s message is exactly what an agent cannot reliably
branch on across a stable, versioned type URI.
Disqualifying Factor: erasing every error to a dynamic type is incompatible with the workspace’s core requirement of a stable, matchable, per-variant RFC 9457 envelope.
Risk Assessment:
- Technical Risk: Low to adopt, but forecloses the workspace’s actual requirement.
- Schedule Risk: Low.
- Ecosystem Risk: High. Defeats the purpose of
mif-problemexisting at all.
Decision
Section titled “Decision”We give each mif-rs crate its own thiserror-derived error enum,
scoped to that crate’s own failure modes, with no shared top-level error
type. Every error-producing crate’s enum implements
mif_problem::ToProblem directly for a uniform RFC 9457 envelope:
mif-schema→MifSchemaErrormif-ontology→OntologyErrormif-frontmatter→FrontmatterErrormif-embed→EmbedErrormif-store→StoreErrormif-cli→CliErrormif-mcp→McpError
Each enum defines its own per-variant mif_problem::ProblemMeta (slug,
version, title, status, exit code) and converts it to a full
ProblemDetails envelope via ProblemMeta::into_details. mif-cli and
mif-mcp wrap each of the five library error types as #[from] variants on
their own CliError/McpError enums.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Variant-level matchability: callers can exhaustively match each
crate’s own error variants, e.g.
matches!(err, OntologyError::Cycle(_))to detect a cyclicextendschain specifically, rather than only a generic failure. - Uniform envelope without a shared hierarchy:
mif_problem::ToProblemgives every crate the same RFC 9457 shape for agent consumers without forcing a shared inheritance/coupling relationship across crates. - Localized extension: adding a new failure mode to one crate is a
one-arm addition to that crate’s own
meta()/to_problem()match, not a change rippling through a type other crates also depend on.
Negative
Section titled “Negative”- Explicit wrapping in the binaries:
mif-cli’s andmif-mcp’s own error enums must explicitly wrap each of the five library error types via#[from]variants, rather than getting a shared conversion for free from a common base error type. This is a fixed, small, enumerable cost (one#[from]variant per library crate) paid once per binary.
Neutral
Section titled “Neutral”- Adding a new library crate to the workspace means adding a new,
independent error enum for it, not extending a shared one. This workspace
deliberately has no shared top-level error type by design — stated
explicitly in
mif-problem’s own module doc comment. mif-problemitself supplies only the envelope shape (ProblemDetails/ProblemMeta/ToProblem), not a base error type — it is not a candidate for becoming the shared error type this decision rejects.
Decision Outcome
Section titled “Decision Outcome”The decision achieves its primary objective — scoped, matchable error enums
sharing one uniform RFC 9457 envelope with no shared top-level error type —
measured by: every one of mif-schema, mif-ontology, mif-frontmatter,
mif-embed, mif-store, mif-cli, and mif-mcp’s error enums implements
mif_problem::ToProblem directly, and none of them depend on a shared
top-level error type crate other than mif-problem itself (which supplies
only the envelope shape, not a base error type). mif-problem’s own module
doc comment states this design intent explicitly:
“This workspace deliberately has no shared top-level error type … —
mif-schema,mif-ontology,mif-frontmatter,mif-embed, andmif-storeeach fail in genuinely different ways and keep their ownthiserrorenum. This crate does not change that: instead of one centralErrorenum with ameta()match …, each crate’s own error enum implementsToProblemdirectly, usingProblemMetato keep its own per-variant type-URI/status/exit-code bookkeeping in one place.”
Related Decisions
Section titled “Related Decisions”- ADR-0003: Virtual Cargo Workspace — establishes the multi-crate workspace structure whose members each own an independent error enum under this decision.
- ADR-0004: Libraries Never Depend on Binaries — the same directional-dependency discipline that keeps library crates from depending on binary crates also keeps them from being forced into a shared error type only the binaries would otherwise motivate.
More Information
Section titled “More Information”- Date: 2026-07-03 (retroactively documents an established, ongoing architectural pattern)
- Source:
crates/mif-problem/src/lib.rsand its consuming crates (crates/mif-schema/src/lib.rs,crates/mif-ontology/src/lib.rs,crates/mif-frontmatter/src/lib.rs,crates/mif-embed/src/lib.rs,crates/mif-store/src/lib.rs,crates/mif-cli/src/main.rs,crates/mif-mcp/src/main.rs)
2026-07-03
Section titled “2026-07-03”Status: Compliant
Findings:
| Finding | Files | Lines | Assessment |
|---|---|---|---|
Module doc comment states “no shared top-level error type” as explicit design intent; every consuming crate’s error enum implements ToProblem directly |
crates/mif-problem/src/lib.rs | 13-22 | accepted |
Summary: Decision matches current, already-implemented practice across all seven error-producing crates; no prior shared error type exists to migrate away from.
Action Required: None — this ADR documents current, already-adopted practice.