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>.
    • 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.
  • When there is callee error to track in a gix-plumbing, use e.g. ExnResult<_, 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. ExnResult<T>.
  • When there is callee error to track in the gix crate, convert both std::error::Error and Exn<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.

TypeDiagnosticClassificationPurpose
MessageVisible message and optional valuesOptionalDescribe a failure without a custom error type
ClassificationMarkerTransparent, no diagnostic of its ownRequiredClassify an existing error while preserving its concrete type
use gix_error::{ErrorExt, Message, MetadataValue};

let error = gix_error::not_found("Reference does not exist")
    .with("path", std::path::Path::new("HEAD"))
    .raise();
assert!(error.is_not_found());
assert!(error.probable_cause().is::<Message>());
assert_eq!(error.metadata().next().expect("lookup details")["path"], MetadataValue::Path("HEAD".into()));

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 gix_error::{message, ResultExt, MetadataValue};

let error = Err::<(), _>(std::io::Error::from(std::io::ErrorKind::NotFound))
    .or_raise(|| message("Could not read reference").with("path", std::path::Path::new("HEAD")))
    .expect_err("the lookup failed");
assert!(error.is_not_found());
assert!(error.probable_cause().is::<std::io::Error>());
assert_eq!(error.error().class, None, "the callee, not the context, supplies the classification");
let values = error.metadata().next().expect("lookup context");
assert_eq!(values["path"], MetadataValue::Path("HEAD".into()));

Exn::metadata() and Error::metadata() yield each message’s non-empty Metadata dictionary in error traversal order. Each dictionary maps names to MetadataValues. 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 gix_error::{ExnMessageResult, ExnResult};

// GOOD — callback bound is flexible, function return is specific:
fn process(cb: impl FnMut() -> ExnResult) -> ExnMessageResult { ... }

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

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 ExnResult<T>, 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
}

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

§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 gix_error::{message, ResultExt, TestResult};

#[test]
fn parses_count() -> TestResult {
    let expected: usize = "42".parse()?;
    let actual = "42".parse::<usize>().or_raise(|| message("could not parse count"))?;
    assert_eq!(actual, expected, "context preserves the parsed count");
    Ok(())
}

§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:
#[error("something went wrong")]
SomethingFailed,
// → Err(Error::SomethingFailed)

// 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 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 Exn<Message>, with a validation class):
ensure!(condition, gix_error::validation("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:
use gix_error::{message, ErrorExt, ExnMessageResult, ResultExt};
fn parse(input: &str) -> ExnMessageResult<Value> { ... }

§Updating tests

Tests of diagnostic wording can use string assertions:

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

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

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 gix_error::{Class, ClassificationMarker, ErrorExt};

let err = ClassificationMarker::with_source(
    Class::Retryable,
    std::io::Error::from(std::io::ErrorKind::AlreadyExists),
).raise();
assert!(err.is_retryable());
assert!(err.probable_cause().is::<std::io::Error>());
assert!(err.downcast_any_ref::<ClassificationMarker>().is_none());

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 gix_error::{ClassificationMarker, ErrorExt};

#[derive(Debug)]
struct MissingObject;

impl std::fmt::Display for MissingObject {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("the requested object is missing from the object database")
    }
}

impl std::error::Error for MissingObject {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(const { &ClassificationMarker::NOT_FOUND })
    }
}

let err = MissingObject.raise();
assert!(err.is_not_found());
assert!(err.probable_cause().is::<MissingObject>());

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 gix_error::{ErrorExt, MetadataValue};

let err = gix_error::validation("invalid input").with("input", b"bad".as_slice()).raise();
let values = err.metadata().find(|values| values.contains_key("input")).expect("input context");
assert_eq!(values["input"], MetadataValue::Bytes("bad".into()));

§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 gix_error::{Class, ClassificationMarker, ErrorExt, message};

let missing_binary_result = Class::Tagged("gix_merge::tree::missing_binary_merge_result");
let err = message("The binary merge result could not be selected")
    .with_class(missing_binary_result)
    .raise()
    .chain(ClassificationMarker::NOT_FOUND)
    .raise(message("Tree merge failed"));

assert!(err.classify().has(missing_binary_result));
assert!(err.is_not_found());

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

ⓘ
fn porcelain_operation() -> Result<(), gix_error::Error> {
    // From<Exn<E>> for Error converts the plumbing error at this boundary.
    plumbing_operation()?;
    Ok(())
}

§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

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

    Without this feature, use into_error() to convert it manually while preserving the error context and caller locations.

  • 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;

Modules§

exn
Frame inspection and type erasure for crate::Exn.
types
Utility types for error-chain interoperability, classification, and diagnostic display.

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§

ClassificationMarker
A transparent classification marker with an optional owned source and no diagnostic of its own.
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.
Message
A diagnostic message with an optional semantic class and named diagnostic values.
TestError
An error for test functions that accepts any error supported by a boxed standard error.

Enums§

Class
The semantic class of an error.
MetadataValue
An owned scalar value in a Metadata dictionary. Bytes and native paths retain their original representation.
ResourceExhaustionKind
The kind of resource exhaustion which prevented an operation from completing.

Traits§

BoxedResultExt
Extension methods for results containing an already boxed error.
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§

allocation_failure
Create a diagnostic for an unrepresentable allocation size or memory that could not be reserved.
allocation_limit
Create a diagnostic for an exceeded application-configured allocation limit.
classify
Lazily inspect the classifications of any borrowed error, including its native sources, I/O payloads and nested crate::Error values. Unknown errors are omitted and distinct causes may yield the same classification.
corruption
Create a diagnostic for malformed or internally inconsistent data, classified as Class::Corruption.
io
Create a diagnostic classified as Class::Io of kind, without an original std::io::Error.
message
Return a new statically allocated message.
not_found
Create a diagnostic for a missing resource, classified as Class::NotFound.
resource_exhaustion
Create a diagnostic for an exhausted resource, classified as Class::ResourceExhaustion of kind.
retryable
Create a diagnostic for an operation that may succeed when retried, classified as Class::Retryable.
validation
Create a diagnostic for invalid function or method input, classified as Class::Validation.

Type Aliases§

ExnMessageResult
A result with a Message exception, defaulting to unit success.
ExnResult
A result with an Exn<E> error, defaulting to unit success and an erased error type.
Metadata
An ordered dictionary of named diagnostic values belonging to a single error context.
Result
A Result type that uses the Error type.
TestResult
A result type for test functions whose errors are reported through TestError’s complete diagnostics.