orion-error
Structured error governance for layered Rust systems.
orion-error is not primarily about prettier error text or local error ergonomics.
It is a Rust crate for systems that need failures to stay structured across layers and boundaries.
Table of Contents
- Why It Is Useful
- Install
- Quick Start
- The 4 APIs To Learn First
- Typical Flow
- Service Boundary Helpers
- Third-Party Error Types
- Standard Error Interop
- Recommended Imports
- Import Strategy
- Error Flow Paths
- Optional Features
- Try It
- Learn More
Why It Is Useful
The design is centered on three parts:
- contract channel — stable identity, category, retryability, visibility
- diagnostic channel — detail, source chain, operation context, key fields
- adaptive output — HTTP / RPC / CLI / log projections generated by policy
In Rust, those ideas land as:
#[derive(OrionError)]for stable semantic identitiesStructError<R>as the unified runtime carriersource_err(...)for first entry and semantic-boundary wrappingconv_err()for reason remapping without rebuilding the error storyreport()/identity_snapshot()/exposure(...)for boundary output
Use this crate when you want:
- one shared error language across service / repo / adapter / protocol layers
- clear business error enums instead of scattered strings
- one consistent way to attach detail, source, and operation context
- stable machine-facing identity for HTTP / RPC / log / CLI boundaries
- controlled bridging to
std::error::Erroronly where needed - a system that scales better than local
Result<T, String>habits
If you only need a small local enum inside one module, thiserror alone may be
enough. If you mainly want application-level convenience with rich ad hoc
context, anyhow may also be enough. orion-error is aimed at systems with
layers, semantic boundaries, and stable boundary-facing error behavior.
In short:
| Crate | Best fit |
|---|---|
thiserror |
Local error modeling inside a single module or crate |
anyhow |
Application-level convenience with ad hoc context |
orion-error |
Project-wide structured error governance across layers |
Install
[]
= "0.8"
Default features include derive and log — add a feature only when you need it.
Quick Start
use From;
use ;
What happens here:
AppReasonis your domain reason enumStructError<AppReason>is the runtime error carriersource_err(...)converts a normal Rust error into the structured systemdoing(...)andwith_context(...)add operation context
For new code, treat doing(...) as the standard operation verb.
The 4 APIs To Learn First
| # | API | When to use |
|---|---|---|
| 1 | #[derive(OrionError)] |
Define stable business-facing reason enums |
| 2 | source_err(reason, detail) |
An error enters the structured system — for both raw std::error::Error and already-structured StructError<_> sources |
| 3 | conv_err() |
Upstream value is already StructError<R1>; you only remap reason type to StructError<R2> |
| 4 | exposure(&policy) |
At service boundaries, project the error into HTTP/RPC/CLI/log output |
Typical Flow
flowchart LR
A[raw std error] -->|source_err| B[StructError R1]
B -->|conv_err| C[StructError R2]
C --> D[report / exposure]
This is the important shift:
- lower layers do not invent random output shapes
- middle layers do not lose source and context
- boundary layers do not re-interpret raw strings
- the whole system shares one governance model
Service Boundary Helpers
When you reach HTTP/RPC/log/CLI boundaries, these are the main entry points:
report()for human-oriented diagnosticsidentity_snapshot()for stable identity inspectionexposure(...)withto_http_error_json(),to_cli_error_json(),to_log_error_json(),to_rpc_error_json()
Current protocol naming is Exposure*, not ErrorPolicy*.
That matters because large systems usually fail at the boundary:
- one team exposes too much detail
- another team hides everything
- every protocol builds its own error schema
orion-error gives those boundaries one consistent projection model.
Third-Party Error Types
source_err supports built-in types (io::Error, serde_json::Error, anyhow::Error,
toml::Error) and custom types via opt-in:
use ;
use *;
;
// Step 1: declare it as a raw source
// Step 2: wrap + convert
let result: = Err;
let err = result
.map_err
.source_err
.unwrap_err;
assert_eq!;
Why opt-in instead of blanket
E: StdError? A blanket impl would silently swallowStructError<_>values as unstructured sources, losing their structured identity and context. The opt-in ensures you explicitly choose which types enter as unstructured sources versus structured ones.
Newtype wrapper for foreign types. If the error type comes from a dependency
and you cannot implement RawStdError directly (orphan rule), use a newtype:
use ;
use *;
;
;
// Usage
let result: = Err;
let err = result
.map_err
.source_err
.unwrap_err;
assert_eq!;
Standard Error Interop
StructError<R> no longer directly implements std::error::Error.
Use the explicit interop APIs when you need that ecosystem:
use ;
let borrowed_err = from;
let owned_err = from;
let boxed_err = from;
let borrowed_std = borrowed_err.as_std;
let owned_std = owned_err.into_std;
let boxed_std = boxed_err.into_boxed_std;
assert!;
assert!;
assert!;
Recommended Imports
For new code, start with:
use *;
Treat this as the default for business code. Only switch to layered imports when the module is explicitly modeling architecture boundaries, protocol adapters, or test/schema checks.
Then add only the layered imports you need, for example:
orion_error::runtime::OperationContextorion_error::runtime::source::*orion_error::report::*orion_error::protocol::*
This keeps normal application code on one predictable entry path while still letting larger codebases keep clear module boundaries where that extra precision is useful.
Import Strategy
Three tiers:
Application code (default)
use *;
use OperationContext;
Architecture boundaries — use layered imports to make module coupling explicit.
// Domain layer
use *;
use ;
// Service / adapter layer — struct error is your carrier
use ;
// Protocol / boundary layer — output projection only
use *;
use ;
// Interop — when you must enter std::error::Error ecosystem
use *;
Test / migration
use *;
use *;
Error Flow Paths
There are exactly four ways a StructError enters or moves through your system:
flowchart LR
A[raw std error / StructError] -->|source_err: first entry| B[Structured system]
B -->|conv_err: reason remap| C[StructError R2]
C --> D[report / exposure]
1. source_err(reason, detail) — unified entry point. Works for both raw
std::error::Error and already-structured StructError sources. Use this
whenever an error enters your system.
2. conv_err() — cross-layer conversion preserving semantics. The upstream error is
already StructError<R1>; you only want to map the reason type to StructError<R2> via
From. All detail, context, source, and metadata survive.
3. as_std() / into_std() / into_dyn_std() — exit point. Bridges the structured error
into the std::error::Error ecosystem for interop or legacy interfaces. These are
explicit; StructError<T> does not implement StdError directly.
Optional Features
Add features only when your project needs them:
| Feature | Purpose |
|---|---|
serde |
Serialize / Deserialize |
serde_json |
Protocol JSON projections |
tracing |
Tracing integration |
anyhow |
anyhow::Error interop |
toml |
toml::Error interop |
[]
= { = "0.8", = ["serde"] } # Serialize/Deserialize
= { = "0.8", = ["serde_json"] } # Protocol JSON projections
= { = "0.8", = ["tracing"] } # Tracing integration
= { = "0.8", = ["anyhow"] } # anyhow::Error interop
= { = "0.8", = ["toml"] } # toml::Error interop
serde, serde_json, tracing, anyhow, toml are optional. The default (derive + log) covers the core path.
Try It
Learn More
- 中文 README
- Changelog
- English docs
- 中文文档
- Tutorial
- Protocol Contract
- thiserror Comparison
- orion-error-derive README
License
Licensed under the MIT License.
Maintainers
If publishing this crate family:
- publish
orion-error-derive - wait for crates.io index propagation
- publish
orion-error