# orion-error
Structured error handling for Rust services with:
- layered universal error categories via `UvsReason`
- domain-specific error enums with stable `ErrorCode`
- contextual propagation via `OperationContext` and `ErrorWith`
- conversion helpers via `ErrorOwe`, `ErrorOweSource`, and `ErrorConv`
- cross-layer wrapping via `WrapStructError` and `ErrorWrap`
- optional source-chain preservation for real underlying errors
[](https://github.com/galaxio-labs/orion-error/actions)
[](https://codecov.io/gh/galaxio-labs/orion-error)
[](https://crates.io/crates/orion-error)
## Installation
```toml
[dependencies]
orion-error = "0.6.1"
```
Optional features:
```toml
[dependencies]
orion-error = { version = "0.6.1", features = ["serde"] }
# or
orion-error = { version = "0.6.1", features = ["tracing"] }
```
Default features include `log`.
## Quick Start
```rust
use derive_more::From;
use orion_error::{
ContextRecord, ErrorCode, ErrorOweSource, ErrorWith, OperationContext, StructError, UvsReason,
};
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq, From)]
enum AppError {
#[error("invalid request")]
InvalidRequest,
#[error("{0}")]
Uvs(UvsReason),
}
impl ErrorCode for AppError {
fn error_code(&self) -> i32 {
match self {
Self::InvalidRequest => 1000,
Self::Uvs(reason) => reason.error_code(),
}
}
}
fn load_config() -> Result<String, StructError<AppError>> {
let mut ctx = OperationContext::want("load_config");
ctx.record("path", "config.toml");
ctx.record_meta("config.kind", "app_config");
ctx.record_meta("config.format", "toml");
std::fs::read_to_string("config.toml")
.owe_sys_source()
.want("read config file")
.with(&ctx)
}
```
Notes:
- `DomainReason` is usually implemented automatically when your enum satisfies `From<UvsReason> + Display + PartialEq`.
- Use `record(...)` on `OperationContext`; `with(...)` on the context itself is deprecated.
- Default to `owe_*_source()` for real error types; use legacy `owe_*()` only when the upstream error is merely `Display`.
- For `Result<T, StructError<_>>`, prefer `err_conv()` or `err_wrap(...)` instead of routing the error back through `owe_*()`.
## Core Concepts
### 1. `UvsReason`
`UvsReason` is the built-in cross-project error taxonomy:
- Business layer: `ValidationError` `100`, `BusinessError` `101`, `NotFoundError` `102`, `PermissionError` `103`, `LogicError` `104`, `RunRuleError` `105`
- Infrastructure layer: `DataError` `200`, `SystemError` `201`, `NetworkError` `202`, `ResourceError` `203`, `TimeoutError` `204`
- Config/external layer: `ConfigError` `300`, `ExternalError` `301`
Useful helpers:
- `error_code()`
- `is_retryable()`
- `is_high_severity()`
- `category_name()`
### 2. `StructError<R>`
`StructError<R>` is the main structured wrapper around a domain reason `R`.
It carries:
- `reason`
- `detail`
- `position`
- context stack
- optional underlying `source`
Construction styles:
```rust
let err = StructError::from(UvsReason::validation_error())
.with_detail("missing field: user_id");
```
```rust
let err = StructError::builder(UvsReason::validation_error())
.detail("missing field: user_id")
.position(location!())
.finish();
```
With preserved source:
```rust
let err = StructError::builder(UvsReason::system_error())
.detail("failed to read config")
.source(std::io::Error::other("disk offline"))
.finish();
```
### 3. Context Propagation
```rust
use orion_error::{ContextRecord, ErrorWith, OperationContext};
let mut ctx = OperationContext::want("process_order");
ctx.record("order_id", "123");
ctx.record("user_id", "42");
let result = do_work()
.want("validate order")
.with(&ctx);
```
Rules of thumb:
- `OperationContext::want("process_order")` defines the outermost goal for this call.
- Chained `.want("validate order")` on an error appends an inner path segment instead of replacing the outer goal.
- Display and `serde` now expose both `Want` and `Path`, for example: `Want=process_order`, `Path=process_order / validate order`.
- Use `target_main()` to read the outermost goal and `target_path()` to read the full path.
### 3.1 Typed Metadata
`OperationContext` can also carry machine-readable metadata for diagnostics and classification:
```rust
use orion_error::{ErrorMetadata, OperationContext, StructError, UvsReason};
let ctx = OperationContext::want("load sink defaults")
.with_meta("config.kind", "sink_defaults")
.with_meta("config.scope", "sink")
.with_meta("parse.line", 1u32);
let err = StructError::from(UvsReason::config_error()).with(ctx);
assert_eq!(err.context_metadata().get_str("config.kind"), Some("sink_defaults"));
```
Recommended usage:
- Put stable classification hints such as `config.kind`, `config.scope`, `component.name`, `parse.line` into metadata.
- Keep metadata short and machine-readable.
- Keep long human-facing explanations in `detail`.
- Metadata is not rendered by default in `Display`.
### 4. Conversion Helpers
Default recommendation for plain `Result<T, E: Error>`:
```rust
read_file().owe_sys_source()?;
http_call().owe_net_source()?;
```
Use legacy `owe_*()` only for sources that are not real error types and only implement `Display`:
```rust
parse_input().owe_validation()?;
message_only_result.owe_biz()?;
```
For converting one `StructError<R1>` into another `StructError<R2>`, prefer `err_conv()`:
```rust
repo_call().err_conv()?;
```
`err_conv()` preserves context, detail, position, and source.
If the upper layer wants to redefine the reason instead of converting it, use `err_wrap(...)` to keep the lower `StructError` as `source`:
```rust
repo_call().err_wrap(UvsReason::system_error())?;
```
In other words:
- `owe_*_source()` is for `Result<T, E>` where `E` is a real non-structured error type
- `err_conv()` is for `Result<T, StructError<R1>>` to `Result<T, StructError<R2>>`
- `err_wrap(...)` is for `Result<T, StructError<R1>>` when the upper layer wants a new reason boundary
If you want to attach a lower `StructError` directly and preserve its structured source frames, use `with_struct_source(...)`:
```rust
use orion_error::{ErrorWith, OperationContext, StructError, UvsReason};
let source = StructError::from(UvsReason::config_error()).with(
OperationContext::want("load sink defaults")
.with_meta("config.kind", "sink_defaults")
);
let err = StructError::from(UvsReason::system_error())
.with(OperationContext::want("start engine").with_meta("component.name", "engine"))
.with_struct_source(source);
assert_eq!(err.context_metadata().get_str("component.name"), Some("engine"));
assert_eq!(
err.source_frames()[0].metadata.get_str("config.kind"),
Some("sink_defaults")
);
```
The same rule applies to the builder API: use `.source_struct(lower_err)` for `StructError<_>` sources, and keep `.source(err)` for ordinary non-structured errors.
## Reports and Redaction
Default `Display` should stay concise. For diagnostics, logs, or structured export, use `ErrorReport` and the explicit render APIs:
```rust
use orion_error::{RedactPolicy, RenderMode, StructError, UvsReason};
struct SimplePolicy;
impl RedactPolicy for SimplePolicy {
fn redact_key(&self, key: &str) -> bool {
matches!(key, "password" | "config.secret")
}
fn redact_value(&self, _key: Option<&str>, _value: &str) -> Option<String> {
Some("<redacted>".to_string())
}
}
let err = StructError::from(UvsReason::config_error())
.with_detail("load config failed")
.with(
orion_error::OperationContext::want("load config")
.with_meta("config.kind", "sink_defaults")
.with_meta("config.secret", "/prod/secrets/api-key"),
);
let report = err.report();
assert_eq!(report.root_metadata.get_str("config.kind"), Some("sink_defaults"));
let verbose = err.render(RenderMode::Verbose);
let redacted = err.render_redacted(RenderMode::Verbose, &SimplePolicy);
assert!(verbose.contains("config.secret"));
assert!(redacted.contains("<redacted>"));
```
Recommended usage:
- `report()` for structured inspection or custom serialization pipelines.
- `render(RenderMode::Compact)` for short summaries.
- `render(RenderMode::Verbose)` for local diagnostics and debug output.
- `render_redacted(...)` before writing potentially sensitive diagnostics to logs or external systems.
## Logging
`OperationContext` supports optional logging integration.
```rust
use orion_error::{op_context, ContextRecord};
let mut ctx = op_context!("sync-user").with_auto_log();
ctx.record("user_id", "42");
ctx.info("starting sync");
do_sync()?;
ctx.mark_suc();
```
Use `scoped_success()` if you want RAII-style success marking.
## Source Chain
If you use `with_source(...)` or `owe_*_source()`, the original error remains available:
```rust
let err: StructError<UvsReason> = std::fs::read_to_string("config.toml")
.owe_sys_source()
.unwrap_err();
assert!(std::error::Error::source(&err).is_some());
assert!(err.root_cause().is_some());
```
You can also inspect the entire chain:
```rust
let chain = err.source_chain();
let frames = err.source_frames();
let pretty = err.display_chain();
```
With the `serde` feature, serialized output also includes:
- `want`
- `path`
- `source_frames`
- `source_message`
- `source_chain`
`source_frames` is the structured form of the chain. Each frame contains:
- `index`
- `message`
- optional `display`
- optional `type_name`
- optional `error_code`
- optional `reason`
- optional `want`
- optional `path`
- optional `detail`
- optional `metadata`
- `is_root_cause`
For `StructError` sources, `message` is the stable reason text and `display` carries the full formatted error. `debug` remains available on `SourceFrame` at runtime, but it is not serialized by default because `Debug` output may contain sensitive internal fields. `source_chain` is kept as a compatibility summary; new observability pipelines should prefer `source_frames`. `type_name` is best-effort and should not be treated as a complete or stable classification key.
The underlying trait object itself is still not serialized.
If you use legacy `owe_*()` helpers, only the display string is copied into `detail`, so they are not the preferred path for normal Rust errors.
## `thiserror` Integration
Recommended pattern:
- use `thiserror` for domain enum definition
- include `Uvs(UvsReason)` as the bridge variant
- implement `ErrorCode`
- use `orion-error` for conversion, context, and classification
See [docs/thiserror-comparison.md](docs/thiserror-comparison.md).
## Migration Notes
Prefer these current names:
- `CwdGuard`-style example does not apply here; ignore older cross-project docs
- `OperationContext::record(...)` instead of deprecated `with(...)`
- `with_auto_log()` instead of deprecated `with_exit_log()`
- prefer `owe_*_source()` by default; keep `owe_*()` for `Display`-only cases
## Validation
From crate root:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features -- --test-threads=1
cargo run --example order_case
cargo run --example logging_example --features log
```
## Chinese Notes
当前版本文档以源码为准,推荐优先参考:
- [docs/tutorial.md](docs/tutorial.md)
- [docs/LOGGING.md](docs/LOGGING.md)
- [docs/thiserror-comparison.md](docs/thiserror-comparison.md)
如果 README 与源码冲突,请以 `src/` 和测试为准。