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
Reportwhen 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:
Reporthas a pointer-sized representation, keepingResult<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 reportCloneable — 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 mutateSee 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:
Report::into_dyn_anyconverts fromReport<C, *, *>toReport<dyn Any, *, *>. Seeexamples/error_coercion.rsfor usage patterns.Report::into_cloneableconverts fromReport<*, Mutable, *>toReport<*, Cloneable, *>. Seeexamples/retry_with_collection.rsfor storing multiple errors.Report::into_localconverts fromReport<*, *, SendSync>toReport<*, *, Local>.
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, *, *>toReport<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. Seeexamples/inspecting_errors.rsfor downcasting techniques.
- You can check if the type of the root node matches a specific type by
using
- From
Report<*, Cloneable, *>toReport<*, 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 callReport::context.
- You can check if the root node only has a single owner using
- From
Report<*, *, *>toReport<PreformattedContext, Mutable, SendSync>:- You can preformat the entire
ReportusingReport::preformat. This creates an entirely newReportthat has the same structure and will look the same as the current one if printed, but all contexts and attachments will be replaced with aPreformattedContextversion.
- You can preformat the entire
§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.
- Report
Iter - An iterator over a report and all its descendant reports in depth-first order.
- Report
Mut - A mutable reference to a
Report. - Report
Ref - A reference to a
Report.
Traits§
- Into
Report - Converts errors and reports into
Reportinstances with specific thread-safety markers. - Into
Report Collection - Converts errors and reports into
ReportCollectioninstances.