rich-error

Crates.iorich-error
lib.rsrich-error
version0.1.0
created_at2025-12-11 17:05:09.478843+00
updated_at2025-12-11 17:05:09.478843+00
descriptionLocation-aware erorrs made easy
homepage
repository
max_upload_size
id1980143
size24,498
Steve Sampson (stphnsmpsn)

documentation

README

rich-error

This crate exposes a single derive, #[derive(RichError)], which turns an enum error type into a location-aware outer error wrapper.

Why RichError?

By default RichError generates an Error wrapper that stores both the inner enum and the std::panic::Location where the error was constructed. This keeps log lines actionable without needing to thread location metadata yourself:

database error: connection refused (at crates/my-crate/src/db.rs:42)

Installation

Add the crate as a procedural-macro dependency. The crate follows the rest of the hummingbird bridge workspace, so you normally depend on it via the workspace Cargo.toml, but for illustration:

[dependencies]
rich-error = { path = "crates/rich-error" }
thiserror = "1"

Usage

Derive RichError on an enum that already implements std::error::Error (e.g. via thiserror::Error). Each enum lives inside its module; the derive will place the generated Error wrapper in the same module.

use thiserror::Error;
use rich_error::RichError;

#[derive(Debug, Error, RichError)]
pub enum DbInner {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("invalid transition from {kind}")]
    InvalidTransition { kind: StatusKind },

    #[error("simple unit error")]
    SimpleUnit,
}

Now you can return Result<T, Error> from the module and construct errors with location-aware helpers:

pub type Result<T> = std::result::Result<T, Error>;

fn do_work() -> Result<()> {
    some_inner_operation()?;               // From<DbInner> for Error
    Err(Error::invalid_transition(kind))?; // struct variant ctor
}

Custom outer type names

If you prefer a module-specific name for the generated wrapper, add the #[rich_error(outer = YourType)] helper attribute alongside the derive:

#[derive(Debug, Error, RichError)]
#[rich_error(outer = BridgeError)]
pub enum BridgeInner {
    #[error("bridge error: {0}")]
    Tuple(sqlx::Error),
}

pub type Result<T> = std::result::Result<T, BridgeError>;

fn do_work() -> Result<()> {
    Err(BridgeError::tuple(sqlx::Error::Protocol("oops".into())))?
}

Generated API (per inner enum)

RichError emits the following items next to the enum:

  • #[derive(Debug)] pub struct Error (or your configured outer type) wrapping the inner enum plus a &'static Location.
  • impl Display that prints <inner display> (at file:line).
  • impl std::error::Error delegating source() to the inner enum.
  • impl Error { #[track_caller] pub fn new(inner: Inner) -> Self }.
  • Per-variant constructors (fn database(...), fn invalid_transition(...), …), each marked #[track_caller] so they record the caller’s location.
  • impl From<Inner> for Error so ? converts automatically.
  • For struct or tuple variants with exactly one field, impl From<FieldTy> for Error, meaning external error types can be ?-propagated straight into the outer error.

Requirements and Caveats

  • Only enums are supported; deriving on structs will emit a compile error.
  • The outer wrapper defaults to Error and is generated in the enum’s module, but you can override it with #[rich_error(outer = MyError)].
  • Because the constructors are plain functions, passing them by name into combinators like Result::map_err(Error::database) records the standard library callsite. Prefer ? or closures (e.g. map_err(|e| Error::database(e))).

Development

cargo test is sufficient: the crate contains only compile-time code and relies on doc-tests for examples.

Commit count: 0

cargo fmt