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>. - When there is callee error to track in a
gix-plumbing, use e.g.Result<_, Exn<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.Result<T, Exn>.
- Remember that
- When there is callee error to track in the
gixcrate, convert bothstd::error::ErrorandExn<E>into [Error]
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]
The baseline that provides a formatted message.
Formatting can more easily be done with the [message!] macro as convenience, roughly equivalent to
Message::new(format!("…")) or format!("…").into().
Specialised types
- [
ValidationError]- like [
Message], but can optionally store the input that caused the failure.
- like [
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 Result<T, Exn> 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 Exn (bare) in closure bounds
Callback and closure bounds should use Result<T, Exn> (bare, without a type parameter)
rather than Result<T, Exn<Message>> or any other specific type. This allows callers to
return any error type from their callbacks without being forced into Message.
Note that functions should still return the most specific type possible (usually Exn<Message>);
only the bound on the callback parameter should use the bare Exn.
// 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 bare Exn, 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()].
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.0.0", path = "../gix-error" }.
Choosing the replacement type
There are two decisions: whether to wrap in [Exn], and which error type to use.
With or without [Exn]:
thiserror enum shape |
Wrap in Exn? |
|---|---|
All variants are simple messages (no #[from]/#[source]) |
No |
Has #[from] or #[source] (wraps callee errors) |
Yes |
Which error type (used directly or as the E in Exn<E>):
| Semantics | Error type |
|---|---|
| General-purpose error messages | [Message] |
| Validation/parsing, optionally storing the offending input | [ValidationError] |
For example, a validation function with no callee errors returns Result<_, ValidationError>,
while a function that wraps I/O errors during parsing could return Result<_, Exn<ValidationError>>.
When in doubt, [Message] is the default choice.
Translating variants
The translation depends on the chosen return type. When the function returns a plain error
type like Result<_, Message>, return the error directly. When it returns Result<_, Exn<_>>,
use .raise() to wrap the error into an [Exn].
Static message variant:
// BEFORE:
SomethingFailed,
// → Err(Error::SomethingFailed)
// AFTER (returning Message):
// → Err(message("something went wrong"))
// AFTER (returning Exn<Message>):
// → Err(message("something went wrong").raise())
Formatted message variant:
// BEFORE:
Unsupported ,
// → Err(Error::Unsupported { format })
// AFTER (returning Message):
// → Err(message!("unsupported format '{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 ValidationError):
ensure!;
// AFTER (returning Exn<Message>):
ensure!;
Updating the function signature
Change the return type, and add the necessary imports:
// BEFORE:
// AFTER (no callee errors wrapped):
// AFTER (callee errors wrapped):
use ;
Updating tests
Pattern-matching on enum variants can be replaced with string assertions:
// BEFORE:
assert!;
// AFTER:
assert_eq!;
To access error-specific metadata (e.g. the input field on [ValidationError]),
use [Exn::downcast_any_ref()] to find a specific error type within the error tree:
// BEFORE:
match result.unwrap_err
// AFTER:
let err = result.unwrap_err;
let ve = err..expect;
assert_eq!;
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 Result<T, Exn>.
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 implement [std::error::Error], which makes it incompatible
with anyhow, Box<dyn Error>, and the ? operator in those contexts.
Instead, convert to [Error] (which does implement std::error::Error) at the boundary:
// In the porcelain crate's error module:
pub type Error = Error; // not gix_archive::Error (which is Exn<Message>)
// The conversion happens automatically via From<Exn<E>> for Error,
// so `?` works without explicit .into_error() calls.
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.