Crate n0_error

Source
Expand description

A library for ergonomic errors with call-site location data

This crate provides a StackError trait and stack_error proc macro to ergonomically work with enum or struct errors.

  • All errors that use the macro will implement the StackError trait, which exposes call-site metadata indicating where the error occurred. Its source method returns an ErrorRef, which is an enum over either a reference to a std::error::Error or another StackError. This allows retrieving error locations for the full error chain, as long as all errors are stack errors.

  • The proc macro can add a meta field to structs or enum variants. This field is the source for the call-site location accessed through the StackError trait. There is a simple declarative macro e! that provides an ergonomic way to construct errors with a meta field without having to spell it out everywhere.

  • This crate also provides an AnyError type, which is similar to anyhow. If constructed from an error that implements StackError, the call-site location is preserved. The AnyError type is generally recommended for applications or tests, whereas libraries should use concrete errors with the macro.

  • There are result extensions to convert StackErrors or std::error::Errors to AnyError while providing additional context.

  • While all errors using the derive macro from this crate have a From implementation for AnyError, regular std errors do not. Unfortunately, a blanket From implementation for all std errors would prevent a specialized implementation for stack errors, causing loss of location metadata upon conversion to AnyError. Therefore, you need to use the std_context or anyerr methods to convert results with std errors to AnyError. You should not use these methods on stack errors; instead, use context or simply forward with ?.

The call-site metadata in the meta field is collected only if the environment variable RUST_BACKTRACE=1 or RUST_ERROR_LOCATION=1 is set. Otherwise, it is not collected, as doing so has a small performance overhead.

Both AnyError and all errors that use the derive macro feature the following outputs:

  • Display impl ({error}) prints only the message of the outermost error failed to process input

  • Alternate display impl ({error:#}) prints the message for each error in the chain, in a single line failed to process input: invalid input: wanted 23 but got 13

  • Debug impl ({error:?}) prints the message and each source, on separate lines.

    failed to process input
    Caused by:
        invalid input
        wanted 23 but got 13

    If RUST_BACKTRACE or RUST_ERROR_LOCATION is set, this will also print the call site of each error.

    failed to process input (examples/basic.rs:61:17)
    Caused by:
         invalid input (examples/basic.rs:36:5)
         wanted 23 but got 13 (examples/basic.rs:48:13)
  • Alternate debug impl {error:#?}: An output similar to how the #[derive(Debug)] output looks.

§Feature flags

  • anyhow (off by default): Enables From<anyhow::Error> for AnyError

§Example

use n0_error::{Result, StackResultExt, StdResultExt, e, stack_error};

/// The `stack_error` macro controls how to turn our enum into a `StackError`.
///
/// * `add_meta` adds a field to all variants to track the call-site error location
/// * `derive` adds `#[derive(StackError)]`
/// * `from_sources` creates `From` impls for the error sources
#[stack_error(derive, add_meta, from_sources)]
enum MyError {
    /// We can define the error message with the `error` attribute
    /// It should not include the error source, those are printed in addition depending on the output format.
    #[error("invalid input")]
    InvalidInput { source: InvalidInput },
    /// Or we can define a variant as `transparent`, which forwards the Display impl to the error source
    #[error(transparent)]
    Io {
        /// For sources that do not implement `StackError`, we have to mark the source as `std_err`.
        #[error(std_err)]
        source: std::io::Error,
    },
}

/// We can use the [`stack_error`] macro on structs as well.
#[stack_error(derive, add_meta)]
#[error("wanted {expected} but got {actual}")]
struct InvalidInput {
    expected: u32,
    actual: u32,
}

fn validate_input(number: u32) -> Result<(), InvalidInput> {
    if number != 23 {
        // The `e` macro constructs a `StackError` while automatically adding the `meta` field.
        Err(e!(InvalidInput {
            actual: number,
            expected: 23
        }))
    } else {
        Ok(())
    }
}

fn fail_io() -> std::io::Result<()> {
    Err(std::io::Error::other("io failed"))
}

/// Some function that returns [`MyError`].
fn process(number: u32) -> Result<(), MyError> {
    // We have a `From` impl for `InvalidInput` on our error.
    validate_input(number)?;
    // We have a `From` impl for `std::io::Error` on our error.
    fail_io()?;
    // Without the From impl, we'd need to forward the error manually.
    // The `e` macro can assist here, so that we don't have to declare the `meta` field manually.
    fail_io().map_err(|source| e!(MyError::Io, source))?;
    Ok(())
}

// A main function that returns AnyError (via the crate's Result alias)
fn run(number: u32) -> Result<()> {
    // We can add context to errors via the result extensions.
    // The `context` function adds context to any `StackError`.
    process(number).context("failed to process input")?;
    // To add context to std errors, we have to use `std_context` from `StdResultExt`.
    fail_io().std_context("failed at fail_io")?;
    Ok(())
}

fn main() -> Result<()> {
    if let Err(err) = run(13) {
        println!("{err}");
        // failed to process input

        println!("{err:#}");
        // failed to process input: invalid input: wanted 23 but got 13

        println!("{err:?}");
        // failed to process input
        // Caused by:
        //     invalid input
        //     wanted 23 but got 13

        // and with RUST_BACKTRACE=1 or RUST_ERROR_LOCATION=1
        // failed to process input (examples/basic.rs:61:17)
        // Caused by:
        //     invalid input (examples/basic.rs:36:5)
        //     wanted 23 but got 13 (examples/basic.rs:48:13)

        println!("{err:#?}");
        // Stack(WithSource {
        //     message: "failed to process input",
        //     source: Stack(InvalidInput {
        //         source: BadNumber {
        //             expected: 23,
        //             actual: 13,
        //             meta: Meta(examples/basic.rs:48:13),
        //         },
        //         meta: Meta(examples/basic.rs:36:5),
        //     }),
        //     meta: Meta(examples/basic.rs:61:17),
        // })
    }
    Ok(())
}

/// You can also use the macros with tuple structs or enums.
/// In this case the meta field will be added as the last field.
#[stack_error(derive, add_meta)]
#[error("tuple fail ({_0})")]
struct TupleStruct(u32);

#[stack_error(derive, add_meta)]
#[allow(unused)]
enum TupleEnum {
    #[error("io failed")]
    Io(#[error(source, std_err)] std::io::Error),
}

Macros§

  • Converts a value into AnyError.
  • Returns an error result by constructing a StackError with e.
  • Returns an error result by constructing an AnyError with anyerr.
  • Constructs an error enum/struct value while automatically filling meta: Meta.
  • Ensures a condition, otherwise returns the error constructed with e from the remaining args.
  • Ensures a condition, otherwise returns an AnyError.
  • Unwraps a result, returning in the error case while converting the error.
  • Unwraps a result, returning in the error case while adding context to the error.

Structs§

Enums§

  • Reference to an error which can either be a std error or a stack error.
  • Output style for rendering error sources in a Report.

Traits§

Functions§

  • Returns a result with the error type set to AnyError.
  • Creates new Meta capturing the caller location.

Type Aliases§

Attribute Macros§

  • Attribute macro to expand error enums or structs.

Derive Macros§

  • Derive macro that implements StackError, Display, Debug and std::error::Error and generates From<T> impls for fields/variants configured via #[error(..)]. Derive macro for stack errors.