#error-handling #bug #explicit-error

macro explicit-error-derive

Derives for crates built on top of explicit-error

4 releases

Uses new Rust 2024

new 0.1.4 May 23, 2025
0.1.3 May 23, 2025
0.1.2 May 22, 2025
0.1.0 May 22, 2025

#44 in #bug

Download history

100 downloads per month
Used in 2 crates

Apache-2.0

14KB
113 lines

Explicit error

Provide tools to have an explicit and concise error syntax for binary crates.

To achieve this goal it provides explicit_error::Error an enum to explicitly differentiates [Bug] errors that cannot panic from Domain errors that return informative feedbacks to users. To generate an idiomatic syntax, explicit-error also provides traits implemented for std::result::Result and std::option::Option .

use explicit_error_exit::{prelude::*, ExitError, derive::ExitError, Result, Bug};
use std::process::ExitCode;

fn business_logic() -> Result<()> {
    let one = Ok::<_, MyError>(())
        .bug()
        .with_context("Usefull context to help debug.")?;

    let two = Some(2).bug()?;

    if 1 < 2 {
        Err(MyError::Foo)?;
    }
    
    Err(MyError::Bar).map_err_or_bug(|e| {
        match e {
            MyError::Foo => Ok(ExitError::new(
                "Informative feedback",
                ExitCode::FAILURE
            )),
            _ => Err(e) // Convert to a Bug with the original error as its std::error::Error source
        }
    })?;

    Ok(())
}

explicit_error::Error is not an opaque type because its Domain variant wraps generic. Usually wrapped types are containers of one error output format that optionnaly carries the underlying error as an std::error::Error source.

Two crates are built on top of explicit-error:

  • explicit-error-http provides tools and derives to idiomatically manage and monitor errors that generate an HTTP response. It has dedicated feature flag to integrate well with most populars web frameworks (actix-web, axum WIP).
  • explicit-error-exit to manage errors that end a process/program.

If you want to have more examples to understand the benefits have a look at explicit-error-http doc and examples.

If you want to use this crate to build you own tooling for your custom error output format. Having a look at how explicit-error-exit is implemented is a good starting point.

If you want to understand the crate's genesis and why it brings something new to the table, read the next two sections.

Comparaison to Anyhow

Anyhow and Explicit both aims to help error management in binary crates but they have opposite trade-offs. The former favor maximum flexibility for implicitness while explicit-error favor explicitness and output format enforcement for less flexibility.

With Anyhow the ? operator can be used on any error that implements std::error::Error in any function returning Result<_, anyhow::Error>. There is no meaningfull information required about what the error means exactly: caller must match on it? domain error? bug?

On the contrary explicit-error::Error is not an opaque type. It is an enum with two variants:

To illustrate, below an example from the explicit-error-http crate.

pub enum Error<D: Domain> {
    Domain(Box<D>), // Box for size: https://doc.rust-lang.org/clippy/lint_configuration.html#large-error-threshold
    Bug(Bug), // Can be generated from any `Result::Err`, `Option::None` or out of the box
}

The chore principle is that the ? operator can be used on errors in functions that return a Result<T, explicit_error::Error<D>> if they are either:

  • marked as Bug
  • convertible to D. Usually D represents the error output format.

For example in the explicit-error-http crate, D is the type HttpError. Any faillible function returning errors that convert to an HTTP response can have as a return type Result<T, explicit_error::Error<HttpError>. To help application's domain errors represented as enums or structs to be convertible to D, crates provide derives to reduce boilerplate.

Below an example from the explicit-error-http crate to show what the syntax looks like.

#[derive(HttpError, Debug)]
enum MyError {
    Foo,
}

impl From<&MyError> for HttpError {
    fn from(value: &MyError) -> Self {
        match value {
            MyError::Foo => HttpError::new(
                    StatusCode::BAD_REQUEST,
                    ProblemDetails::new()
                        .with_type(Uri::from_static("/errors/my-domain/foo"))
                        .with_title("Foo format incorrect.")
                ),
        }
    }
}

fn business_logic() -> Result<()> {
    // Error from a library that should not happen
    Err(sqlx::Error::RowNotFound)
        .bug()?;

    // Application error
    if 1 > 2 {
        Err(MyError::Foo)?;
    }

    // Inline error
    Err(42).map_err(|_|
        HttpError::new(
            StatusCode::BAD_REQUEST,
            ProblemDetails::new()
                .with_type(Uri::from_static("/errors/business-logic"))
                .with_title("Informative feedback for the user."),
        )
    )?;

    Ok(())
}

As you can see, function's error flow logic is straightforward, explicit and remain concise!

Most of the time, for "lib functions", relying on the caller to generate a proper domain error, the best implementation is to return a dedicated error type for idiomatic pattern matching. For rare cases when you have to pattern match on the source error of an explict_error::Error, explict_error::try_map_on_source can be used.

Comparaison to ThisError

Thiserror is a great tool for library errors. That's why it supplements well with explicit-error which is designed for binary crates.

Why only relying on it for application can be a footgun?

When using ThisError you naturally tend to use enums as errors everywhere and heavily rely on the derive #[from] to have conversion between types giving the ability to use the ? operator almost everywhere without thinking.

It is fine in small applications as the combinatory between errors remains limited. But as the code base grows everything becomes more and more implicit. Understand the error logic flow starts to be really painfull as you have to read multiple implementations spread in different places.

Moreover boilerplate and coupling increase (also maintenance cost) because enums have multiple meaningless variants from a domain point of view:

  • encapsulate types from dependencies
  • represent stuff that should not happen (aka Bug) and cannot panic.

Finally, it is also painfull to have consistency in error output format and monitoring:

  • Without a type to unified errors the implementation are spread
  • With one type to unify errors (eg: a big enum called AppError), cohesion is discreased with more boilerplate

Dependencies

~185–610KB
~15K SLoC