Crate rootcause

Crate rootcause 

Source
Expand description

A flexible, ergonomic, and inspectable error reporting library for Rust.

§Overview

This crate provides a structured way to represent and work with errors and their context. The main goal is to enable you to build rich, structured error reports that automatically capture not just what went wrong, but also the context and supporting data at each step in the error’s propagation.

Unlike simple string-based error messages, rootcause allows you to attach typed data to errors, build error chains, and inspect error contents programmatically. This makes debugging easier while still providing beautiful, human-readable error messages.

§Quick Example

use rootcause::prelude::{Report, ResultExt};

fn read_config(path: &str) -> Result<String, Report> {
    std::fs::read_to_string(path).context("Failed to read configuration file")?;
    Ok(String::new())
}

For more examples, see the examples directory in the repository. Start with basic.rs for a hands-on introduction.

§Core Concepts

At a high level, rootcause helps you build a tree of error reports. Each node in the tree represents a step in the error’s history - you start with a root error, then add context and attachments as it propagates up through your code.

Most error reports are linear chains (just like anyhow), but the tree structure lets you collect multiple related errors when needed.

Each report has:

  • A context (the error itself)
  • Optional attachments (debugging data)
  • Optional children (one or more errors that caused this error)

For implementation details, see the rootcause-internals crate.

§Project Goals

  • Ergonomic: The ? operator should work with most error types, even ones not designed for this library.
  • Multi-failure tracking: When operations fail multiple times (retry attempts, batch processing, parallel execution), all failures should be captured and preserved in a single report.
  • Inspectable: The objects in a Report should not be glorified strings. Inspecting and interacting with them should be easy.
  • Optionally typed: Users should be able to (optionally) specify the type of the context in the root node.
  • Beautiful: The default formatting should look pleasant—and if it doesn’t match your style, the hook system lets you customize it.
  • Cloneable: It should be possible to clone a Report when you need to.
  • Self-documenting: Reports should automatically capture information (like backtraces and locations) that might be useful in debugging.
  • Customizable: It should be possible to customize what data gets collected, or how reports are formatted.
  • Lightweight: Report has a pointer-sized representation, keeping Result<T, Report> small and fast.

§Report Type Parameters

The Report type is generic over three parameters, but for most users the defaults work fine.

Most common usage:

// Just use Report - works like anyhow::Error
fn might_fail() -> Result<(), Report> {
}

For type safety:

#[derive(Debug)]
struct MyError;

// Use Report<YourError> - works like error-stack
fn typed_error() -> Result<(), Report<MyError>> {
}

Need cloning or thread-local data? The sections below explain the other type parameters. Come back to these when you need them - they solve specific problems you’ll recognize when you encounter them.


§Type Parameters

This section covers the full type parameter system. Most users won’t need these variants immediately - but if you do need cloning, thread-local errors, or want to understand what’s possible, read on.

The Report type has three type parameters: Report<Context, Ownership, ThreadSafety>. This section explains all the options and when you’d use them.

§Context Type: Typed vs Dynamic Errors

Use Report<dyn Any> (or just Report) when errors just need to propagate. Use Report<YourErrorType> when callers need to pattern match on specific error variants.

Report<dyn Any> (or just Report) — Flexible, like anyhow

Can hold any error type at the root. The ? operator automatically converts any error into a Report. Note: dyn Any is just a marker - no actual trait object is stored. Converting between typed and dynamic reports is zero-cost.

// Can return any error type
fn might_fail() -> Result<(), Report> {
}

Report<YourErrorType> — Type-safe, like error-stack

The root error must be YourErrorType, but child errors can be anything. Callers can use .current_context() to pattern match on the typed error.

#[derive(Debug)]
struct ConfigError {/* ... */}

// This function MUST return ConfigError at the root
fn load_config() -> Result<(), Report<ConfigError>> {
}

See examples/typed_reports.rs for a complete example with retry logic.

§Ownership: Mutable vs Cloneable

Use the default (Mutable) when errors just propagate with ?. Use .into_cloneable() when you need to store errors in collections or use them multiple times.

Mutable (default) — Unique ownership

You can add attachments and context to the root, but can’t clone the whole Report. Note: child reports are still cloneable internally (they use Arc), but the top-level Report doesn’t implement Clone. Start here, then convert to Cloneable if you need to clone the entire tree.

let mut report: Report<String, markers::Mutable> = report!("error".to_string());
let report = report.attach("debug info"); // ✅ Can mutate root
// let cloned = report.clone();           // ❌ Can't clone whole report

Cloneable — Shared ownership

The Report can be cloned cheaply (via Arc), but can’t be mutated. Use when you need to pass the same error to multiple places.

let report: Report<String, markers::Mutable> = report!("error".to_string());
let cloneable = report.into_cloneable();
let copy1 = cloneable.clone(); // ✅ Can clone
let copy2 = cloneable.clone(); // ✅ Cheap (Arc clone)
// let modified = copy1.attach("info"); // ❌ Can't mutate

See examples/retry_with_collection.rs for collection usage.

§Thread Safety: SendSync vs Local

Use the default (SendSync) unless you get compiler errors about Send or Sync. Use Local only when attaching !Send types like Rc or Cell.

SendSync (default) — Thread-safe

The Report and all its contents are Send + Sync. Most types (String, Vec, primitives) are already Send + Sync, so this just works.

let report: Report<String, markers::Mutable, markers::SendSync> = report!("error".to_string());

std::thread::spawn(move || {
    println!("{}", report); // ✅ Can send to other threads
});

Local — Not thread-safe

Use when your error contains thread-local data like Rc, raw pointers, or other !Send types.

use std::rc::Rc;

let data = Rc::new("thread-local".to_string());
let report: Report<Rc<String>, markers::Mutable, markers::Local> = report!(data);
// std::thread::spawn(move || { ... }); // ❌ Can't send to other threads

§Converting Between Report Variants

The variant lists above have been ordered so that it is always possible to convert to an element further down the list using the From trait. This also means you can use ? when converting downwards. There are also more specific methods (implemented using From) to help with type inference and to more clearly communicate intent:

On the other hand, it is generally harder to convert to an element further up the list. Here are some of the ways to do it:

  • From Report<dyn Any, *, *> to Report<SomeContextType, *, *>:
    • You can check if the type of the root node matches a specific type by using Report::downcast_report. This will return either the requested report type or the original report depending on whether the types match. See examples/inspecting_errors.rs for downcasting techniques.
  • From Report<*, Cloneable, *> to Report<*, Mutable, *>:
    • You can check if the root node only has a single owner using Report::try_into_mutable. This will check the number of references to the root node and return either the requested report variant or the original report depending on whether it is unique.
    • You can allocate a new root node and set the current node as a child of the new node. The new root node will be Mutable. One method for allocating a new root node is to call Report::context.
  • From Report<*, *, *> to Report<PreformattedContext, Mutable, SendSync>:
    • You can preformat the entire Report using Report::preformat. This creates an entirely new Report that has the same structure and will look the same as the current one if printed, but all contexts and attachments will be replaced with a PreformattedContext version.

§Acknowledgements

This library was inspired by and draws ideas from several existing error handling libraries in the Rust ecosystem, including anyhow, thiserror, and error-stack.

Modules§

compat
Compatibility and interoperability with other error handling libraries.
handlers
Handlers that control how errors and attachments are formatted and displayed.
hooks
Hooks system for customizing report creation and formatting behavior.
markers
Marker types and traits for defining ownership and thread-safety semantics.
preformatted
Preformatted context and attachment types.
prelude
Commonly used items for convenient importing.
report_attachment
Individual attachments for error reports.
report_attachments
Collections of report attachments.
report_collection
Collections of reports.

Macros§

bail
Returns early from a function with an error report.
report
Creates a new error report.
report_attachment
Creates a report attachment with contextual data.

Structs§

Report
An error report that contains a context, child reports, and attachments.
ReportIter
An iterator over a report and all its descendant reports in depth-first order.
ReportMut
A mutable reference to a Report.
ReportRef
A reference to a Report.

Traits§

IntoReport
Converts errors and reports into Report instances with specific thread-safety markers.
IntoReportCollection
Converts errors and reports into ReportCollection instances.