orion-error
Structured error handling for Rust services with:
- layered universal error categories via
UvsReason - domain-specific error enums with stable
OrionErroridentities - contextual propagation via
OperationContextandErrorWith - first-entry conversion via
IntoAs - structured cross-layer wrapping via
ErrorWrapAs - optional source-chain preservation for real underlying errors
Installation
[]
= "0.7.0"
Optional features:
[]
= { = "0.7.0", = ["serde"] }
# or
= { = "0.7.0", = ["tracing"] }
# or
= { = "0.7.0", = ["serde_json"] }
# or
= { = "0.7.0", = ["anyhow"] }
# or
= { = "0.7.0", = ["toml"] }
Default features include log and derive.
StructError<R> no longer implements std::error::Error. Standard-error
ecosystem boundaries should use the explicit bridge APIs instead:
let owned_std = err.clone.into_std;
let borrowed_std = err.as_std;
let boxed_std = err.into_boxed_std;
Default builds should use source_ref(), report(), snapshot(), or the
bridge APIs instead of calling std::error::Error::source(&err) directly on
StructError<R>.
Current docs:
- CHANGELOG.md
- docs/tutorial.md
- docs/reason-identity-guide.md
- docs/protocol-contract.md
- docs/stable-snapshot-schema.md
- docs/thiserror-comparison.md
- orion-error-derive/README.md
Release order on crates.io:
- Publish
orion-error-derivefirst. - Wait for crates.io index propagation.
- Publish
orion-error.
Import guidance:
orion_error::prelude::*is the primary convenience wildcard import and intentionally exports only the main path:OrionError,StructError,IntoAs,ErrorWith,ErrorWrapAs, andDefaultExposurePolicy.- Small root imports such as
orion_error::{StructError, OrionError}are preferred when you want explicit imports for the main path only. orion_error::advanced_prelude::*is only for advanced protocol/schema checks and migration tests.- Layered imports are available when code needs stricter responsibility boundaries:
orion_error::runtime::*orion_error::conversion::*orion_error::snapshot::*orion_error::report::*orion_error::bridge::*orion_error::reason::*orion_error::testcase::*
orion_error::compat_prelude::*/orion_error::compat_traits::*are explicit legacy compatibility imports forowe(...)
For new code, prefer orion_error::prelude::* plus small layered imports for examples, and small root imports for production modules. Use layered imports when the module benefits from explicit runtime / snapshot / report / bridge / testcase boundaries.
Recommended import split:
reason::*forErrorCode,ErrorCategory,ErrorIdentityProvider,UvsReasonreport::*forVisibility, projection response types, and projection/rendering APIssnapshot::*for stable snapshot schema constantsbridge::*forraw_sourceandRawStdErrortestcase::*forassert_err_identity(...)and other test helpers
Root exceptions that are still reasonable:
ErrorCodeandErrorIdentityProviderremain valid root imports because those names are also derive-macro entry points.
Recommended API
Current primary names:
DefaultExposurePolicyExposurePolicyExposureDecisionExposureViewexposure_view()exposure_snapshot()to_exposure_snapshot_json()
Feature matrix
deriveEnables#[derive(OrionError)].logEnablesOperationContextlog integration.tracingSwitchesOperationContextlogging totracing.serdeEnables serde support for runtime, report, and snapshot structures.serde_jsonEnables JSON helper methods such asto_stable_snapshot_json()andto_exposure_snapshot_json().anyhowEnablesanyhow::Errorintegration forinto_as(...).tomlEnables TOML error integration forinto_as(...).
Quick Start
use From;
use ;
Notes:
DomainReasonis implemented byOrionError; reason enums should deriveOrionErrorinstead of relying on structural blanket impls.- Derive
OrionErroron domain enums and declare stableidentitywith#[orion_error(...)]. - Use
record_field(...)/record_meta(...)onOperationContext;with_context(...)is the primary error-side API for full context frames. - Default to
into_as(...)for supported plain error sources entering the structured system the first time. - Use
wrap_as(...)when the upstream value is alreadyStructError<_>and the upper layer wants a new reason boundary. - Runtime propagation uses
StructError; stable machine export usesStableErrorSnapshot; human diagnostics useDiagnosticReport. - For export-layer work, prefer
snapshot().stable_export()or, with theserde_jsonfeature,snapshot().to_stable_snapshot_json(). - For human-facing diagnostics and redaction, use
report()/render(...)/render_redacted(...). - Use
into_std()/OwnedStdStructError::from(err)/as_std()/StdStructRef::from(&err)when explicitly bridging aStructError<_>into the standard error ecosystem. - Use
OwnedStdStructError::into_struct()when you need to come back from the owned bridge to the structured runtime carrier. - Use
into_dyn_std()only when an owned, type-erased official bridge is required, such as ananyhow::Errorboundary that must later be recognized byinto_as(...). - Use
into_boxed_std()when a boundary requiresBox<dyn std::error::Error + Send + Sync>. - Use
source_payload()/source_payload_kind()only for read-only inspection of the source payload branch. - Use legacy
owe(...)only as a compatibility path forDisplay-only values. - Prefer
with_std_source(...)/with_struct_source(...)andsource_std(...)/source_struct(...)in new code so the source branch stays explicit.with_source(...)andbuilder.source(...)remain available as compatibility helpers for automatic routing.
Core Concepts
1. UvsReason
UvsReason is the built-in cross-project error taxonomy:
- Business layer:
ValidationError100,BusinessError101,NotFoundError102,PermissionError103,LogicError104,RunRuleError105 - Infrastructure layer:
DataError200,SystemError201,NetworkError202,ResourceError203,TimeoutError204 - Config/external layer:
ConfigError300,ExternalError301
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:
reasondetailposition- context stack
- optional underlying
source
Construction styles:
let err = from
.with_detail;
let err = builder
.detail
.position
.finish;
With preserved source:
let err = builder
.detail
.source
.finish;
For non-structured sources on an existing StructError, prefer:
let err = from
.with_detail
.with_std_source;
3. Context Propagation
use ;
let mut ctx = doing;
ctx.record_field;
ctx.record_field;
let result = do_work
.doing
.with_context;
Rules of thumb:
OperationContext::doing("process_order")is the primary naming path for the outermost goal.- Chained
.doing("validate order")on an error appends an inner path segment instead of replacing the outer goal. doing(...)writes the structuredactionfield and keepstarget/pathas the compatibility projection;want(...)is a compatibility alias.- Use
action_main()/locator_main()to read the primary semantics; usetarget_main()/target_path()when you need the compatibility projection. - Display and
serdenow expose bothWantandPath, for example:Want=process_order,Path=process_order / validate order.
3.1 Typed Metadata
OperationContext can also carry machine-readable metadata for diagnostics and classification:
use ;
use ErrorMetadata;
let ctx = doing
.with_meta
.with_meta
.with_meta;
let err = from.with_context;
assert_eq!;
Recommended usage:
- Put stable classification hints such as
config.kind,config.scope,component.name,parse.lineinto 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> entering the structured system:
use ;
read_file.into_as?;
http_call.into_as?;
Use raw_source(...) only when you must explicitly mark a downstream opt-in raw StdError type as unstructured:
use fmt;
use ;
;
let third_party_call = ;
third_party_call
.map_err
.into_as?;
raw_source(...) is intentionally conservative. It only accepts types that explicitly implement RawStdError; it is not a blanket E: StdError path, and it must not be used for StructError<_>.
This is the intended design:
IntoAsstays behind a sealedUnstructuredSourceentry- built-in allowlisted raw errors implement
UnstructuredSourcedirectly - unknown downstream raw
StdErrortypes may opt in explicitly throughRawStdError StructError<_>cannot enterraw_source(...), because downstream crates cannot implementRawStdErrorfor external types
In other words, the explicit escape hatch is kept without reopening a blanket E: StdError path.
With the anyhow feature, anyhow::Error is still treated as an aggregated but unstructured error by default. The only structured exception is a top-level official OwnedDynStdStructError created from StructError<_>::into_dyn_std(). orion-error does not scan arbitrary anyhow source chains and does not guess third-party wrappers.
Use legacy owe(...) only when maintaining values that are not real error types and only implement Display. Import it from the explicit compat module:
use ;
message_only_result.owe?;
other_message_only_result.owe?;
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 wrap_as(...) to keep the lower StructError as source:
use ;
repo_call.wrap_as?;
In other words:
into_as(...)is forResult<T, E>whereEis a real non-structured error typeerr_conv()is forResult<T, StructError<R1>>toResult<T, StructError<R2>>wrap_as(...)is forResult<T, StructError<R1>>when the upper layer wants a new reason boundaryerr_wrap(...)/wrap(...)are compatibility helpers; preferwrap_as(...)in new code
If you want to attach a lower StructError directly and preserve its structured source frames, use with_struct_source(...):
use ;
let source = from.with_context;
let err = from
.with_context
.with_struct_source;
assert_eq!;
assert_eq!;
The same rule applies to the builder API: use .source_struct(lower_err) for StructError<_> sources, and .source_std(err) for ordinary non-structured errors.
Reports and Redaction
Default Display should stay concise. Use this separation:
- Runtime propagation uses
StructError. - Stable machine export uses
StableErrorSnapshot. - Human diagnostics and redaction use
DiagnosticReport.
Most application code can stay on StructError and call the high-level helpers:
use ;
;
let err = from
.with_detail
.with_context;
let report = err.report;
assert_eq!;
let verbose = err.render;
let redacted = err.render_redacted;
assert!;
assert!;
Recommended usage:
snapshot().stable_export()or, with theserde_jsonfeature,snapshot().to_stable_snapshot_json()for stable machine export.report()for human diagnostic inspection.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 op_context;
use op_context;
let mut ctx = op_context!.with_auto_log;
ctx.record_field;
ctx.info;
do_sync?;
ctx.mark_suc;
Use scoped_success() if you want RAII-style success marking.
Source Chain
If you use with_std_source(...), raw_source(...), or into_as(...), the original error remains available:
use ;
let err: = read_to_string
.into_as
.unwrap_err;
assert!;
assert!;
assert!;
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, the default Serialize for StructError remains a compatibility runtime projection. It still includes:
wantpathsource_framessource_messagesource_chain
source_frames is the structured form of the chain. Each frame contains:
indexmessage- 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. For new export paths, prefer err.snapshot(), err.report(), or the stable snapshot JSON helpers.
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 Interop
thiserror is no longer required for the recommended path. Prefer OrionError for domain reasons because it generates display text, stable identity, category, and the legacy numeric code from one annotation.
Use thiserror only when an existing enum already depends on std::error::Error behavior or external APIs require a standard error type.
See docs/thiserror-comparison.md.
Migration Notes
Prefer these current names:
CwdGuard-style example does not apply here; ignore older cross-project docsOperationContext::record_field(...)instead of deprecatedwith(...)with_auto_log()instead of deprecatedwith_exit_log()- prefer
into_as(reason, detail)for realStdErrorsources - keep
owe(...)only for legacyDisplay-only cases
Validation
From crate root:
Chinese Notes
当前版本文档以源码为准,推荐优先参考:
如果 README 与源码冲突,请以 src/ 和测试为准。