Common error types and utilities for error handling.
Usage
- When there is no callee error to track, use simple
std::error::Errorimplementations directly, e.g.Result<_, Simple>.- If call-site tracking is important, prefer
ExnResult<_, Simple>instead: [Exn] stores the location where the error was raised, which plain error values do not.
- If call-site tracking is important, prefer
- When there is callee error to track in a
gix-plumbing, use e.g.ExnResult<_, Simple>.- Remember that
Exn<T>does not implementstd::error::Errorso it's not easy to use outsidegix-crates. - Use the type-erased version in callbacks like [
Exn] (without type arguments), i.e.ExnResult<T>.
- Remember that
- When there is callee error to track in the
gixcrate, convert bothstd::error::ErrorandExn<E>into [Error]
ExnResult<T, E> abbreviates a result with an Exn<E> error. Its defaults are
T = () and E = exn::Untyped, matching bare [Exn]. Use ExnMessageResult<T> for message contexts.
Standard Error Types
These should always be used if they match the meaning of the error well enough instead of creating an own
Error-implementing type, and used with
ResultExt::or_raise(<StandardErrorType>) or
OptionExt::ok_or_raise(<StandardErrorType>), or sibling methods.
All these types implement Error.
[Message] and [ClassificationMarker]
[Message] combines a diagnostic message, an optional [Class], and named scalar values. Use it
instead of a chain of type-bearing errors when those layers only provide the category and details of a single
failure. [not_found()], [validation()], [corruption()], [retryable()], [resource_exhaustion()],
[allocation_limit()], [allocation_failure()], and [io()] construct classified messages.
[message()] and [Message::new()] start without a class or values. [Message::with_class()] and
[Message::with()] add them to the same diagnostic. Use [message!] for formatting, equivalent to
Message::new(format!("…")) or format!("…").into().
Classification does not determine which diagnostic values can be attached. For example,
corruption("Malformed reference").with("input", bytes) preserves offending bytes in the same
error that describes their corruption. No extra validation error is needed just to store input.
Use explicit classified constructors: converting a string to [Message] does not infer a class
from the function's return type.
| Type | Diagnostic | Classification | Purpose |
|---|---|---|---|
[Message] |
Visible message and optional values | Optional | Describe a failure without a custom error type |
[ClassificationMarker] |
Transparent, no diagnostic of its own | Required | Classify an existing error while preserving its concrete type |
use ;
let error = not_found
.with
.raise;
assert!;
assert!;
assert_eq!;
Callers should add context using information they already possess and document its keys on the function that returns it. Preserve real callee errors, especially concrete recovery signals and complex results discovered by the callee, such as partial outcomes:
use ;
let error = Err::
.or_raise
.expect_err;
assert!;
assert!;
assert_eq!;
let values = error.metadata.next.expect;
assert_eq!;
[Exn::metadata()] and [Error::metadata()] yield each message's non-empty [Metadata] dictionary in error traversal order.
Each dictionary maps names to [MetadataValue]s. Keys are local to their context; dictionaries from independent causes
are never combined. To identify a specific failure without inspecting its values, see
matching a specific failure.
Exn<ErrorType> and [Exn]
The [Exn] type does not implement Error itself, but is able to store causing errors
via [ResultExt::or_raise()] (and sibling methods) as well as location information of the creation site.
While plumbing functions that need to track causes should always return a distinct type like Exn<Message>,
if that's not possible, use [Exn::erased] to let it return ExnResult<T> instead, allowing any return type.
A side effect of this is that any callee that causes errors needs to be annotated with
.or_raise(|| message!("context information")) or .or_raise_erased(|| message!("context information")).
Using [ExnResult] in closure bounds
Callback and closure bounds should use ExnResult<T> (without an explicit error type)
rather than ExnMessageResult<T> or any other specific type. This allows callers to
return any error type from their callbacks without being forced into Message.
Functions should still return the most specific type possible (usually ExnMessageResult<T>);
only the bound on the callback parameter should use the default, erased error type.
use ;
// GOOD — callback bound is flexible, function return is specific:
// BAD — forces caller to construct Message errors in their callback:
Inside the function, use .or_raise() to convert the bare Exn from the
callback into the function's typed error, adding context:
let entry = callback.or_raise?;
Inside a closure that must return ExnResult<T>, use .or_erased() to
convert a typed Exn<E> to Exn, or raise_erased() for standalone errors:
|stream|
[Error] — Exn with std::error::Error
Since [Exn] does not implement [std::error::Error], it cannot be used where that trait is required
(e.g. std::io::Error::other(), or as a #[source] in another error type).
The [Error] type bridges this gap: it implements [std::error::Error] and converts from any
Exn<E> via [From], preserving the full error tree and location information.
// Convert an Exn to something usable as std::error::Error:
let exn: = message.raise;
let err: Error = exn.into;
let err: Error = exn.into_error;
// Useful where std::error::Error is required:
other
It can also be created directly from any std::error::Error via [Error::from_error()].
Tests with [TestResult]
Return [TestResult] from #[test] functions to propagate ordinary errors, Exn<E>, and [Error]
directly with ?. It defaults to Result<(), TestError>; helpers returning a value can use TestResult<T>.
Accepted errors must convert into Box<dyn std::error::Error + Send + Sync + 'static>.
When a test returns an error, Rust's test harness prints [TestError]'s Debug output,
including the complete diagnostic tree or chain and captured caller locations.
use ;
Migrating from thiserror
This section describes the mechanical translation from thiserror error enums to gix-error.
In Cargo.toml, replace thiserror = "<version>" with gix-error = { version = "^0.1.0", path = "../gix-error" }.
Choosing the replacement type
Use [ExnMessageResult] for diagnostic messages, including validation failures without callee errors.
[Message] carries an optional class and named scalar values; [Exn] retains the diagnostic context and causes.
Keep a concrete error type in [ExnResult] when recovery requires its specific payload.
Use [Result] at porcelain boundaries that return [Error].
Use the chosen type directly in signatures, importing it under its canonical name where helpful.
Crate-specific and operation-specific forwarding aliases or renamed error exports are unnecessary.
Facades may re-export the canonical types, as gix does with Error, Exn, Result, ExnResult, and ExnMessageResult.
Always import the result aliases directly and use their bare names in signatures.
Translating variants
Use .raise() to wrap standalone errors into an [Exn], and
[ResultExt::or_raise()] to preserve callee errors with additional context.
Static message variant:
// BEFORE:
SomethingFailed,
// → Err(Error::SomethingFailed)
// AFTER (returning Exn<Message>):
// → Err(message("something went wrong").raise())
Formatted message variant:
// BEFORE:
Unsupported ,
// → Err(Error::Unsupported { format })
// AFTER (returning Exn<Message>):
// → Err(message!("unsupported format '{format:?}'").raise())
#[from] / #[error(transparent)] variant — delete the variant;
at each call site, use [ResultExt::or_raise()] to add context:
// BEFORE:
Io,
// → something_that_returns_io_error()? // auto-converted via From
// AFTER (the variant is deleted):
// → something_that_returns_io_error()
// .or_raise(|| message("context about what failed"))?
#[source] variant with message — use [ResultExt::or_raise()]:
// BEFORE:
Config,
// → Err(Error::Config(err))
// AFTER:
// → config_call().or_raise(|| message("failed to parse config"))?
Guard / assertion — use [ensure!]:
// BEFORE:
if !condition
// AFTER (returning Exn<Message>, with a validation class):
ensure!;
// AFTER (returning Exn<Message>):
ensure!;
Updating the function signature
Change the return type, and add the necessary imports:
// BEFORE:
// AFTER:
use ;
Updating tests
Tests of diagnostic wording can use string assertions:
// BEFORE:
assert!;
// AFTER:
assert_eq!;
For semantic checks, both [Exn] and [Error] provide is_retryable(),
is_not_found(), is_validation(),
is_corrupted(), and is_resource_exhausted().
These inspect causes as well as the outermost error. is_retryable() requires an explicit retry classification;
[Exn::can_retry()] and [Error::can_retry()] additionally recognize certain I/O error kinds.
Use [Exn::probable_cause()] to inspect the likely root cause. It follows a single causal path, stopping at the
first branch rather than choosing an arbitrary sibling. Classification markers are transparent to this selection.
[Exn::classify()] and [Error::classify()] expose each known classification together with its original error.
Custom payloads of [std::io::Error] are inspected too, including any nested [Error] trees.
[Message] supplies its own diagnostic and optional classification. In contrast, [ClassificationMarker]
only supplies classification metadata. Use [ClassificationMarker::with_source()] to classify an existing error
while preserving its concrete type:
use ;
let err = with_source.raise;
assert!;
assert!;
assert!;
Custom error types preserve classifications by exposing their immediate cause as Some(inner) from
[std::error::Error::source()]. Forwarding to inner.source() instead can hide a classification carried by
inner itself. A custom leaf error can borrow a constant such as [ClassificationMarker::NOT_FOUND]
as its source to preserve its classification without defining a static or adding a generic category to its diagnostic:
use ;
;
let err = MissingObject.raise;
assert!;
assert!;
Use classification predicates rather than downcasting to [Message] just to recognize
a category: diagnostic iterators and downcasts skip all classification markers. Exception and test reports
omit their wrappers too, while raw [std::error::Error::source()] chains retain them. Genuine classified errors
remain causal and can still be downcast to inspect their payloads. When storing an [Exn] in a custom error, convert it with
[Exn::into_error()] so the source can expose its complete tree.
To access scalar diagnostics such as offending input, inspect the documented metadata key:
use ;
let err = validation.with.raise;
let values = err.metadata.find.expect;
assert_eq!;
Matching a specific failure
Use [Class::Tagged] when a broad category such as [Class::NotFound] isn't specific enough for recovery.
A single stable, namespaced tag identifies the condition without a custom error type or metadata matching.
Functions returning tagged errors document their tags as part of their recovery contract, independently of
diagnostic wording. A tag implies no other classification. When a general class also applies, chain a
[ClassificationMarker] to retain it without adding a visible diagnostic.
use ;
let missing_binary_result = Tagged;
let err = message
.with_class
.raise
.chain
.raise;
assert!;
assert!;
[types::Classifications::has()] also finds tagged causes through wrapping contexts and [Error] conversion.
Matching one cause does not make other failures in an aggregate ignorable.
Common Pitfalls
Don't use .erased() to change the Exn type parameter
[Exn::raise()] already nests the current Exn<E> as a child of a new Exn<T>,
so there is no need to erase the type first. Use [ErrorExt::and_raise()] as shorthand:
// WRONG — double-boxes and discards type information:
io_err.raise.erased.raise
// OK — raise() nests the Exn<io::Error> as a child of Exn<Message> directly:
io_err.raise.raise
// BEST — and_raise() is a shorthand for .raise().raise():
io_err.and_raise
Only use .erased() when you genuinely need a type-erased Exn (no type parameter),
e.g. to return different error types from the same function via ExnResult<T>.
Don't use .raise_all() with a single error
[Exn::raise_all()] is meant for creating error trees with multiple causes.
If you only have a single causing error, use .or_raise() instead:
// WRONG — raise_all() is for multiple causes, not a single one:
result.map_err?;
// RIGHT — or_raise() wraps the error with context directly:
result.or_raise?;
Convert Exn to [Error] at public API boundaries
Porcelain crates (like gix) should not expose Exn<Message> in their public API
because it does not itself implement [std::error::Error].
Instead, convert to [Error] (which does implement std::error::Error) at the boundary.
[Exn] also converts directly into Box<dyn std::error::Error + Send + Sync>, so ? works
without an explicit conversion when that is the receiving result's error type:
Supporting types
Frequently used error types, extension traits, result aliases, and constructors are available at the crate root.
Utility types for flattened chains, classification, and diagnostic display live in [types]. Exception frames
and the default type-erasure marker live in [exn]; [Exn] and its extension traits are only exported at the root.
Feature Flags
Why not anyhow?
anyhow is a proven and optimized library, and it would certainly suffice for an error-chain based approach
where users are expected to downcast to concrete types.
What's missing though is track-caller which will always capture the location of error instantiation, along with
compatibility for error trees, which are happening when multiple calls are in flight during concurrency.
Both libraries share the shortcoming of not being able to implement std::error::Error on their error type,
and both provide workarounds.
exn is much less optimized, but also costs only a Box on the stack,
which in any case is a step up from thiserror which exposed a lot of heft to the stack.