Crate anyerr

Source
Expand description

anyerr is a comprehensive error handling library designed to offer flexibility, extensibility, and an ergonomic way to handle errors in Rust applications.

This library provides a central AnyError type that can carry arbitrary error information, including a custom error kind, a backtrace, contextual data and so on. It enables developers to create error types composing different levels of errors without sacrificing the ability to preserve rich context information.

§Key Features

  • Error Composition: Wrap and combine errors while preserving their original information and access the underlying errors if needed.
  • Customizable Error Kind: Make use of predefined error kinds offered by this crate or define your own error kinds by implementing the Kind trait.
  • Contextual Data: Attach rich context information to errors using different pluggable context types.
  • Backtrace Support: Automatically captures backtraces for easier debugging.
  • Error Reporting: Customize and write formated and detailed error messages to stdout, loggers and so on.

§Getting Started

§Defining a Custom Error Type

AnyError is the core of this crate. It works in a way resembled to Box<dyn Error>, by implementing the Error trait and leverage the functionality of the Any trait, while it’s also Send and Sync, allowing safely accesses across multiple concurrent threads. AnyError is easy to get started with, though, it’s not somthing like Box<dyn Error> that can be used directly in your codebase, but a highly customizable type requiring you to make decisions about its components.

An AnyError<C, K> has two generic type parameters C and K, stand for the context storage and the error kind respectively.

AbstractContext is implemented for C, so is Context usually but it’s not required. With C implementing Context, you can attach additional contextual data to an AnyError for better debugging. An example of one of the most useful contexts is LiteralKeyStringMapContext, which holds entries of a &'static str and String pair structure, and stores the the Debug representation of values.

K is required to implement the trait Kind, specifying a general kind of the error. Although a structured error handling style is not preferred under this circumstance, an error kind enables more fine-grained logging and tracing or enhances experience of other aspects. DefaultErrorKind is a Kind provided by this crate, and the design of its variant is based on the author’s web backend developemnt experience.

Once you have chosen the components you need, you can define your custom error type, by supplying AnyError with the selected context and error kind. Here’s an example:

// Make this module accessible to your whole crate.
mod err {
    use anyerr::AnyError as AnyErrorTemplate;
    use anyerr::context::LiteralKeyStringMapContext;

    pub use anyerr::{Intermediate, Overlay}; // These are helper traits.
    pub use anyerr::kind::DefaultErrorKind as ErrKind;
    pub use anyerr::Report;

    pub type AnyError = AnyErrorTemplate<LiteralKeyStringMapContext, ErrKind>;
    pub type AnyResult<T> = Result<T, AnyError>;
}
// Include this in wherever you need `AnyError`.
use err::*;

§Creating and Using Errors

Here’s how to create AnyError in your application:

use err::*;

fn fail() -> AnyResult<()> {
    // Use `AnyError::minimal()` to create a simple [`String`]-based error.
    Err(AnyError::minimal("this function always fails"))
}

fn check_positive(x: i32) -> AnyResult<()> {
    if x > 0 {
        return Ok(());
    }
    // Use `AnyError::quick()` to quickly create an error with an error
    // message and an error kind.
    Err(AnyError::quick(
        "expects `x` to be a positive number",
        ErrKind::ValueValidation
    ))
}

fn try_add_username(
    usernames: &mut Vec<String>,
    new_username: String
) -> AnyResult<usize> {
    let res = usernames.iter()
        .enumerate()
        .find(|(_, username)| **username == new_username)
        .map(|(index, _)| index);
    if let Some(index) = res {
        // Use `AnyError::builder()` to create an error with all essential
        // context you'll need.
        let err = AnyError::builder()
            .message("the username already exists")
            .kind(ErrKind::RuleViolation)
            .context("new_username", new_username)
            .context("index", index)
            .build();
        Err(err)
    } else {
        usernames.push(new_username);
        Ok(usernames.len() - 1)
    }
}

fn parse_i32(input: &str) -> AnyResult<i32> {
    // Use `AnyError::wrap()` to wrap any other error type.
    input.parse::<i32>().map_err(AnyError::wrap)
}

Let’s take the third function try_add_username() as an example to demonstrate how we can use AnyError:

use err::*;

fn main() {
    let mut usernames = Vec::new();

    let res = try_add_username(&mut usernames, "foo").unwrap();
    assert_eq!(res, 0);

    let err = try_add_username(&mut usernames, "foo").unwrap_err();
    assert_eq!(err.to_string(), "the username already exists"); // Or `err.message()`.
    assert_eq!(err.kind(), ErrKind::RuleViolation);
    assert_eq!(err.get("new_username"), Some("\"foo\""));
    assert_eq!(err.get("index"), Some("0"));
}

§Error Wrapping and Chaining

The AnyError type supports convenient error wrapping, allowing you to maintain the original error while adding additional context. Methods in the Overlay and Intermediate helper traits provides ergonomic means for you to make an overlay of your existing error and attach rich context to it.

Say we’d like to reteive a User entity by its username from the UserRepository. It’s acknowledged that the query may fails due to a variety of reasons, but we don’t care about the details but whether we could get that entity. The following codeblock demonstrates this idea.

use err::*;

struct UserRepository {
    conn: Arc<Connection>,
}

impl UserRepository {
    pub fn find_by_username(&self, username: &str) -> AnyResult<User> {
        // Don't build SQL statements yourself in practice.
        let statement = format!("SELECT * FROM users WHERE users.username = '{username}'");
        let data = self.conn.query(&statement)
            .overlay(("could not get a `User` due to SQL execution error", ErrKind::EntityAbsence))
            .context("username", username)
            .context("statement", statement)?;
        let entity = User::try_from(data)
            .overlay(("could not get a `User` due to serialization error", ErrKind::EntityAbsence))
            .context("username", username)?;
        Ok(entity)
    }
}

§Error Reporting

You might have the experience that you wrote the code which iterated over the error chain and formated causes. It’s pretty tedious to manually and repeatly write such code. Therefore, this crate does this for you by providing Report. Report captures your function’s result and then you can output the error report directly to terminals, loggers or whatever.

use err::*;

fn source_error() -> AnyResult<()> {
    let err = AnyError::builder()
        .message("the source error is here")
        .kind(ErrKind::InfrastructureFailure)
        .context("key1", "value1")
        .context("key2", "value2")
        .build();
    Err(err)
}

fn intermediate_error() -> AnyResult<()> {
    source_error()
        .overlay("the intermediate error is here")
        .context("key3", "value3")?;
    Ok(())
}

fn toplevel_error() -> AnyResult<()> {
    intermediate_error()
        .overlay("the toplevel error is here")?;
    Ok(())
}

let report1 = Report::wrap(toplevel_error().unwrap_err()).pretty(false);
let report2 = Report::capture(|| -> AnyResult<()> { toplevel_error() });
println!("Error: {report1}");
println!("{report2}");

The output of report1:

Error: (Unknown) the toplevel error is here: (Unknown) the intermediate error is here: (InfrastructureFailure) the source error is here [key3 = "value3", key1 = "value1", key2 = "value2"]

The output of report2:

Error:
    (InfrastructureFailure) the toplevel error is here
Caused by:
    (Unknown) the intermediate error is here
    [key3 = "value3"]
Caused by:
    (Unknown) the source error is here
    [key1 = "value1", key2 = "value2"]

Stack backtrace:
  0: anyerr::core::data::ErrorDataBuilder<C,K>::build
            at ./src/core/data.rs:210:28
  1: anyerr::core::AnyErrorBuilder<C,K>::build
            at ./src/core.rs:415:24
  2: anyerr::source_error
            at ./src/main.rs:18:15
  3: anyerr::intermediate_error
            at ./src/main.rs:28:5
  4: anyerr::toplevel_error
            at ./src/main.rs:35:5
  5: anyerr::main::{{closure}}
            at ./src/main.rs:40:43
  6: anyerr::report::Report<C,K>::capture
            at ./src/report.rs:52:15
  7: anyerr::main
            at ./src/main.rs:40:5
   ...

Using Report in main()’s returning position is also allowed:

use std::process::Termination;
use err::*;

fn main() -> impl Termination {
    Report::capture(|| {
        toplevel_error()?;
        Ok(())
    })
}

For more information about error reporting customization, see the documentations of Report.

§Advanced Usage

§Different Context Types

This crate allows using different context types, such as SingletonContext, StringContext, AnyContext or the ones you developed by yourself, depending on how you want to manage and retrieve additional information from your errors. It’s even viable that you don’t want your error type to carry a context storage, through the NoContext trait. Each context type offers unique capabilities for structuring error metadata.

For more information, refer to the types in the crate::context module.

§Usage without an Error Kind

For some reasons, you may not want each error to have an error kind. This crate offers you NoErrorKind, which actually has only one variant as its default value. By selecting NoErrorKind, you no longer need to do anything with error kinds.

Re-exports§

pub use core::AnyError;
pub use overlay::Intermediate;
pub use overlay::Overlay;
pub use report::Report;

Modules§

context
converter
core
kind
overlay
report