Crate error_stack

source ·
Expand description

A context-aware error library with arbitrary attached user data.

crates.io libs.rs rust-version discord

Overview

error-stack is an error-handling library centered around the idea of building a Report of the error as it propagates. A Report is made up of two concepts:

  1. Contexts
  2. Attachments

A Context is a view of the world, it helps describe how the current section of code interprets the error. This is used to capture how various scopes require differing levels of detail and understanding of the error as it propagates. A Report always captures the current context in its generic argument.

As the Report is built, various pieces of supporting information can be attached. These can be anything that can be shared between threads whether it be a supporting message or a custom-defined Suggestion struct.

Quick-Start Guide

Where to use a Report

Report has been designed to be used as the Err variant of a Result. This crate provides a Result<E, C> type alias for convenience which uses Report<C> as the Err variant and can be used as a return type:

use error_stack::{ensure, Result};

fn main() -> Result<(), AccessError> {
    let user = get_user()?;
    let resource = get_resource()?;

    ensure!(
        has_permission(user, resource),
        AccessError::PermissionDenied(user, resource)
    );

    ...
}

Initializing a Report

A Report can be created directly from anything that implements Context by using Report::new() or through any of the provided macros (report!, bail!, ensure!). Any Error can be used as a Context, so it’s possible to create Report from an existing Error:

use std::{fs, io, path::Path};

use error_stack::Report;

// Note: For demonstration purposes this example does not use `error_stack::Result`.
// As can be seen, it's possible to implicitly convert `io::Error` to `Report<io::Error>`
fn read_file(path: impl AsRef<Path>) -> Result<String, Report<io::Error>> {
    let content = fs::read_to_string(path)?;

    ...
}

Using and Expanding the Report

As mentioned, the library centers around the idea of building a Report as it propagates.

Changing Context

The generic parameter in Report is called the current context. When creating a new Report, the Context that’s provided will be set as the current context. The current context should encapsulate how the current code interprets the error. As the error propagates, it will cross boundaries where new information is available, and the previous level of detail is no longer applicable. These boundaries will often occur when crossing between major modules, or when execution crosses between crates. At this point the Report should start to operate in a new context. To change the context, Report::change_context() is used:

(Again, for convenience, using ResultExt will do that on the Err variant)

use error_stack::{Context, Result, ResultExt};

#[derive(Debug)]
struct ParseConfigError;

impl fmt::Display for ParseConfigError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt.write_str("could not parse configuration file")
    }
}

// It's also possible to implement `Error` instead.
impl Context for ParseConfigError {}

// For clarification, this example is not using `error_stack::Result`.
fn parse_config(path: impl AsRef<Path>) -> Result<Config, ParseConfigError> {
    let content = fs::read_to_string(path.as_ref())
        .change_context(ParseConfigError)?;

    ...
}

Building up the Report - Attachments

Module/crate boundaries are not the only places where information can be embedded within the Report however. Additional information can be attached within the current context, whether this be a string, or any thread-safe object. These attachments are added by using Report::attach() and Report::attach_printable():

struct Suggestion(&'static str);

fn parse_config(path: impl AsRef<Path>) -> Result<Config, Report<ParseConfigError>> {
    let path = path.as_ref();

    let content = fs::read_to_string(path)
        .change_context(ParseConfigError::new())
        .attach(Suggestion("use a file you can read next time!"))
        .attach_printable_lazy(|| format!("could not read file {path:?}"))?;

    Ok(content)
}

As seen above, there are ways on attaching more information to the Report: attach and attach_printable. These two functions behave similar, but the latter has a more restrictive bound on the attachment: Display and Debug. Depending on the function used, printing the Report will also use the Display and Debug traits to describe the attachment.

This outputs something like:

could not parse configuration file
├╴at src/lib.rs:25:10
├╴could not read file "test.txt"
├╴1 additional opaque attachment
│
╰─▶ No such file or directory (os error 2)
    ├╴at src/lib.rs:25:10
    ╰╴backtrace (1)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

backtrace no. 1
  [redacted]

The Suggestion which was added via attach is not shown directly and only increases the counter of opaque attachments for the containing Context. The message which was passed to attach_printable, however, is displayed in full. To be able to show attachments that have been added via attach, one must make use of hooks instead.

Multiple Errors

Report supports the combination and propagation of multiple errors natively. This is useful in cases like parallel processing where multiple errors might happen independently from each other, in these use-cases you are able to use the implementations of Extend and extend_one() and are able to propagate all errors instead of just a single one.


fn parse_configs(paths: &[impl AsRef<Path>]) -> Result<Vec<Config>, Report<std::io::Error>> {
    let mut configs = Vec::new();
    let mut error: Option<Report<std::io::Error>> = None;

    for path in paths {
        let path = path.as_ref();

        match fs::read_to_string(path) {
            Ok(ok) => {
                configs.push(ok);
            }
            Err(err) => {
                if let Some(error) = error.as_mut() {
                    error.extend_one(err.into());
                } else {
                    error = Some(err.into());
                }
            }
        }
    }

    if let Some(error) = error {
        return Err(error);
    }

    Ok(configs)
}

In-Depth Explanation

Crate Philosophy

This crate adds some development overhead in comparison to other error handling strategies, especially around creating custom root-errors (specifically error-stack does not allow using string-like types). The intention is that this reduces overhead at other parts of the process, whether that be implementing error-handling, debugging, or observability. The idea that underpins this is that errors should happen in well-scoped environments like reading a file or parsing a string into an integer. For these errors, a well-defined error type should be used (i.e. io::Error or ParseIntError) instead of creating an error from a string. Requiring a well-defined type forces users to be conscious about how they classify and group their custom error types, which improves their usability in error-handling.

Improving Result::Err Types

By capturing the current Context in the type parameter, return types in function signatures continue to explicitly capture the perspective of the current code. This means that more often than not the user is forced to re-describe the error when entering a substantially different part of the code because the constraints of typed return types will require it. This will happen most often when crossing module/crate boundaries.

An example of this is a ConfigParseError when produced when parsing a configuration file at a high-level in the code vs. the lower-level io::Error that occurs when reading the file from disk. The io::Error may no longer be valuable at the level of the code that’s handling parsing a config, and re-framing the error in a new type allows the user to incorporate contextual information that’s only available higher-up in the stack.

Compatibility with other Libraries

In std (or nightly) environments a blanket implementation for Context for any Error is provided. This blanket implementation for Error means error-stack is compatible with almost all other libraries that use the Error trait.

This has the added benefit that migrating from other error libraries can often be incremental, as a lot of popular error library types will work within the Report struct.

In addition, error-stack supports converting errors generated from the anyhow or eyre crate via IntoReportCompat.

Doing more

Beyond making new Context types, the library supports the attachment of arbitrary thread-safe data. These attachments (and data that is provided by the Context can be requested through Report::request_ref(). This gives a novel way to expand standard error-handling approaches, without decreasing the ergonomics of creating the actual error variants:

fn main() {
    if let Err(report) = parse_config("config.json") {
        for suggestion in report.request_ref::<Suggestion>() {
            eprintln!("suggestion: {}", suggestion.0);
        }
    }
}

Additional Features

The above examples will probably cover 90% of the common use case. This crate does have additional features for more specific scenarios:

Automatic Backtraces

When on a Rust 1.65 or later, Report will try to capture a Backtrace if RUST_BACKTRACE or RUST_BACKTRACE_LIB is set. If on a nightly toolchain, it will use the Backtrace if provided by the base Context, and will try to capture one otherwise.

Unlike some other approaches, this does not require the user modifying their custom error types to be aware of backtraces, and doesn’t require manual implementations to forward calls down any wrapped errors.

No-Std compatible

The complete crate is written for no-std environments, which can be used by setting default-features = false in Cargo.toml.

Provider API

This crate uses the Provider API to provide arbitrary data. This can be done either by attaching them to a Report or by providing it directly when implementing Context. The blanket implementation of Context for Error will provide any data provided by Error::provide.

To request a provided type, Report::request_ref or Report::request_value are used. Both return an iterator of all provided values with the specified type. The value, which was provided most recently will be returned first.

Macros for Convenience

Three macros are provided to simplify the generation of a Report.

  • report! will only create a Report from its parameter. It will take into account if the passed type itself is a Report or a Context. For the former case, it will retain the details stored on a Report, for the latter case it will create a new Report from the Context.
  • bail! acts like report! but also immediately returns the Report as Err variant.
  • ensure! will check an expression and if it’s evaluated to false, it will act like bail!.

Span Traces

The crate comes with built-in support for tracings SpanTrace. If the spantrace feature is enabled and an ErrorLayer is set, a SpanTrace is either used when provided by the root Context or will be captured when creating the Report.

Debug Hooks

One can provide hooks for types added as attachments when the std feature is enabled. These hooks are then used while formatting Report. This functionality is also used internally by error-stack to render Backtrace, and SpanTrace, which means overwriting and customizing them is as easy as providing another hook.

You can add new hooks with Report::install_debug_hook. Refer to the module-level documentation of fmt for further information.

Additional Adaptors

ResultExt is a convenient wrapper around Result<_, impl Context> and Result<_, Report<impl Context>. It offers attach and change_context on the Result directly, but also a lazy variant that receives a function which is only called if an error happens.

In addition to ResultExt, this crate also comes with FutureExt, which provides the same functionality for Futures.

Colored output and charset selection

You can override the color support by using the Report::set_color_mode. To override the charset used, you can use Report::set_charset. The default color mode is emphasis. The default charset is UTF-8.

To automatically detect support if your target output supports unicode and colors you can check out the detect.rs example.

Feature Flags

FeatureDescriptiondefault
stdEnables support for Error, and, on Rust 1.65+, Backtraceenabled
spantraceEnables automatic capturing of SpanTracesdisabled
hooksEnables hooks on no-std platforms using spin locksdisabled
anyhowProvides into_report to convert anyhow::Error to Reportdisabled
eyreProvides into_report to convert eyre::Report to Reportdisabled

Modules

  • Implementation of formatting, to enable colors and the use of box-drawing characters use the pretty-print feature.
  • Extension for convenient usage of Reports returned by Future s.
  • Iterators over Frames.

Macros

Structs

Enums

  • Classification of an attachment which is determined by the method it was created in.
  • Classification of the contents of a Frame, determined by how it was created.

Traits

Type Aliases