Skip to main content

Crate gix_error

Crate gix_error 

Source
Expand description

Common error types and utilities for error handling.

§Usage

  • When there is no callee error to track, use simple std::error::Error implementations 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 implement std::error::Error so it’s not easy to use outside gix- crates.
    • Use the type-erased version in callbacks like Exn (without type arguments), i.e. Result<T, Exn>.
  • When there is callee error to track in the gix crate, convert both std::error::Error and Exn<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

§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:
fn process(cb: impl FnMut() -> Result<(), Exn>) -> Result<(), Exn<Message>> { ... }

// BAD — forces caller to construct Message errors in their callback:
fn process(cb: impl FnMut() -> Result<(), Exn<Message>>) -> Result<(), Exn<Message>> { ... }

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(|| message("context about the callback call"))?;

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| {
    stream.next_entry().or_erased()   // Exn<Message> → Exn
}

§ErrorExn 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: Exn<Message> = message("something failed").raise();
let err: gix_error::Error = exn.into();
let err: gix_error::Error = exn.into_error();

// Useful where std::error::Error is required:
std::io::Error::other(exn.into_error())

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

SemanticsError type
General-purpose error messagesMessage
Validation/parsing, optionally storing the offending inputValidationError

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:
#[error("something went wrong")]
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:
#[error("unsupported format '{format:?}'")]
Unsupported { format: Format },
// → 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:
#[error(transparent)]
Io(#[from] std::io::Error),
// → 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:
#[error("failed to parse config")]
Config(#[source] config::Error),
// → Err(Error::Config(err))

// AFTER:
// → config_call().or_raise(|| message("failed to parse config"))?

Guard / assertion — use ensure!:

// BEFORE:
if !condition {
    return Err(Error::SomethingFailed);
}

// AFTER (returning ValidationError):
ensure!(condition, ValidationError::new("something went wrong"));

// AFTER (returning Exn<Message>):
ensure!(condition, message("something went wrong"));

§Updating the function signature

Change the return type, and add the necessary imports:

// BEFORE:
fn parse(input: &str) -> Result<Value, Error> { ... }

// AFTER (no callee errors wrapped):
fn parse(input: &str) -> Result<Value, Message> { ... }

// AFTER (callee errors wrapped):
use gix_error::{message, ErrorExt, Exn, Message, ResultExt};
fn parse(input: &str) -> Result<Value, Exn<Message>> { ... }

§Updating tests

Pattern-matching on enum variants can be replaced with string assertions:

// BEFORE:
assert!(matches!(result.unwrap_err(), Error::SomethingFailed));

// AFTER:
assert_eq!(result.unwrap_err().to_string(), "something went wrong");

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() {
    Error::InvalidInput { input } => assert_eq!(input, "bad"),
    other => panic!("unexpected: {other}"),
}

// AFTER:
let err = result.unwrap_err();
let ve = err.downcast_any_ref::<ValidationError>().expect("is a ValidationError");
assert_eq!(ve.input.as_deref(), Some("bad".into()));

§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(message("context"))

// OK — raise() nests the Exn<io::Error> as a child of Exn<Message> directly:
io_err.raise().raise(message("context"))

// BEST — and_raise() is a shorthand for .raise().raise():
io_err.and_raise(message("context"))

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(|e| message("context").raise_all(Some(e.raise())))?;

// RIGHT — or_raise() wraps the error with context directly:
result.or_raise(|| message("context"))?;

§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 = gix_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

  • anyhow — The Exn type converts to anyhow::Error natively so ? can be used directly.

    Otherwise, it would have to be manually converted via into_box() or into_inner().

  • auto-chain-error — The Error type is always flattening the Exn error tree into a chain of errors, while keeping their locations and runtime type-information.

  • tree-error — The opposite of auto-chain-error and implicitly enabled by default. Use it to override auto-chain-error.

§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.

Re-exports§

pub use bstr;

Macros§

bail
Creates an Exn and returns it as Result.
ensure
Ensures $cond is met; otherwise return an error.
message
Construct a Message from a string literal or format string. Note that it always runs format!(), use the message() function for literals instead.

Structs§

ChainedError
A generic error which represents a linked-list of errors and exposes it with source(). It’s meant to be the target of a conversion of any Exn error tree.
Error
An error type that wraps an inner type-erased boxed std::error::Error or an Exn frame.
Exn
An exception type that can hold an error tree and the call site.
Frame
A frame in the exception tree.
Message
An error that is further described in a message.
Something
An error that merely says that something is wrong.
Untyped
A marker to show that type information is not available, while storing all extractable information about the erased type. It’s the default type for Exn.
ValidationError
An error occurred when validating input.

Traits§

ErrorExt
A trait bound of the supported error type of Exn.
OptionExt
An extension trait for Option to provide raising new exceptions on None.
ResultExt
An extension trait for Result to provide context information on Exns.

Functions§

message
Return a new statically allocated message.