orion-error 0.6.2

Struct Error for Large Project
Documentation

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

CI Coverage Status crates.io

Installation

[dependencies]
orion-error = "0.6.1"

Optional features:

[dependencies]
orion-error = { version = "0.6.1", features = ["serde"] }
# or
orion-error = { version = "0.6.1", features = ["tracing"] }

Default features include log.

Quick Start

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:

let err = StructError::from(UvsReason::validation_error())
    .with_detail("missing field: user_id");
let err = StructError::builder(UvsReason::validation_error())
    .detail("missing field: user_id")
    .position(location!())
    .finish();

With preserved source:

let err = StructError::builder(UvsReason::system_error())
    .detail("failed to read config")
    .source(std::io::Error::other("disk offline"))
    .finish();

3. Context Propagation

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:

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>:

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:

parse_input().owe_validation()?;
message_only_result.owe_biz()?;

For converting one StructError<R1> into another StructError<R2>, prefer err_conv():

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:

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(...):

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:

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.

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:

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:

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.

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:

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

当前版本文档以源码为准,推荐优先参考:

如果 README 与源码冲突,请以 src/ 和测试为准。