SkillAgentSearch skills...

Rootcause

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

Install / Use

/learn @rootcause-rs/Rootcause
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

rootcause

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

Build Status Crates.io Documentation Discord License: MIT/Apache-2.0

<img src="https://github.com/rootcause-rs/rootcause/raw/main/rootcause.png" width="192">

Overview

rootcause helps you build rich, structured error reports that capture not just what went wrong, but the full context and history.

Here's a simple example (from examples/basic.rs) showing how errors build up context as they propagate through your call stack:

use rootcause::prelude::*;

fn read_file(path: &str) -> Result<String, Report> {
    // The ? operator automatically converts io::Error to Report
    let content = std::fs::read_to_string(path)?;
    Ok(content)
}

fn load_config(path: &str) -> Result<String, Report> {
    // Add context to explain what this file is for
    let content = read_file(path)
        .context("Failed to load application configuration")?;
    Ok(content)
}

fn load_config_with_debug_info(path: &str) -> Result<String, Report> {
    // Attach debugging information
    let content = load_config(path)
        .attach(format!("Config path: {path}"))
        .attach("Expected format: TOML")?;
    Ok(content)
}

fn startup(config_path: &str, environment: &str) -> Result<(), Report> {
    let _config = load_config_with_debug_info(config_path)
        .context("Application startup failed")
        .attach(format!("Environment: {environment}"))?;
    Ok(())
}

When startup() fails, you get a chain showing the full story:

 ● Application startup failed
 ├ examples/basic.rs:76:10
 ├ Environment: production
 │
 ● Failed to load application configuration
 ├ examples/basic.rs:47:35
 ├ Config path: /nonexistent/config.toml
 ├ Expected format: TOML
 │
 ● No such file or directory (os error 2)
 ╰ examples/basic.rs:34:19

Each layer adds context and debugging information, building a trail from the high-level operation down to the root cause.

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.

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

Why rootcause?

Collecting Multiple Errors

When operations fail multiple times (retries, batch processing, parallel tasks), rootcause lets you gather all the failures into a single tree structure with readable output (from examples/retry_with_collection.rs):

use rootcause::{prelude::*, report_collection::ReportCollection};

fn fetch_document_with_retry(url: &str, retry_count: usize) -> Result<Vec<u8>, Report> {
    let mut errors = ReportCollection::new();

    for attempt in 1..=retry_count {
        match fetch_document(url).attach_with(|| format!("Attempt #{attempt}")) {
            Ok(data) => return Ok(data),
            // Make error cloneable so we can store it (see "Supporting..." section below)
            Err(error) => errors.push(error.into_cloneable()),
        }
    }

    Err(errors.context(format!("Unable to fetch document {url}")))?
}

The output from the above function will be a tree with data associated to each node:

 ● Unable to fetch document http://example.com
 ├ examples/retry_with_collection.rs:59:16
 │
 ├─ ● HTTP error: 500 Internal server error
 │  ├ examples/retry_with_collection.rs:42:9
 │  ╰ Attempt #1
 │
 ╰─ ● HTTP error: 404 Not found
    ├ examples/retry_with_collection.rs:42:9
    ╰ Attempt #2

For more tree examples, see retry_with_collection.rs and batch_processing.rs.

Inspecting Error Trees

Errors aren't just formatted strings—they're structured objects you can traverse and analyze programmatically. This enables analytics, custom error handling, and automated debugging (from examples/inspecting_errors.rs):

use rootcause::prelude::*;

// Analyze retry failures to extract structured data
fn analyze_retry_failures(report: &Report) -> Vec<(u32, u16)> {
    let mut attempts = Vec::new();

    // Traverse all nodes in the error tree
    for node in report.iter_reports() {
        // Try to downcast the context to NetworkError
        if let Some(network_err) = node.downcast_current_context::<NetworkError>() {
            // Search through attachments for RetryMetadata
            for attachment in node.attachments().iter() {
                if let Some(retry_meta) = attachment.downcast_inner::<RetryMetadata>() {
                    attempts.push((retry_meta.attempt, network_err.status_code));
                }
            }
        }
    }

    attempts
}

This lets you extract retry statistics, categorize errors, or build custom monitoring—not just display them. See inspecting_errors.rs for complete examples.

Supporting Advanced Use Cases

When you need to use the same error in multiple places—like sending to a logging backend and displaying to a user, potentially on different threads—you can make errors cloneable:

use rootcause::prelude::*;

// Make the error cloneable so we can use it multiple times
let error = fetch_data().unwrap_err().into_cloneable();

// Send to background logging service
let log_error = error.clone();
tokio::spawn(async move {
    log_to_backend(log_error).await;
});

// Also display to user
display_error(error);

For the niche case where you're working with !Send errors from other libraries or need to attach thread-local data:

// Include thread-local data in error reports
let report: Report<_, _, Local> = report!(MyError)
    .attach(Rc::new(expensive_data));

Most code uses the defaults, but these type parameters are available when you need them. See Report Type Parameters for details.

Type-Safe Error Handling

Libraries often need to preserve specific error types so callers can handle errors programmatically. Use Report<YourError> to enable pattern matching without runtime type checks (simplified from examples/typed_reports.rs):

use rootcause::prelude::*;

// Library function returns typed error
fn query_database(id: u32) -> Result<Data, Report<DatabaseError>> {
    // ... can fail with specific error types
}

// Caller can pattern match to handle errors intelligently
fn process_with_retry(id: u32) -> Result<Data, Report> {
    match query_database(id) {
        Ok(data) => Ok(data),
        Err(report) => {
            // Pattern match on the typed context
            match report.current_context() {
                DatabaseError::ConnectionLost | DatabaseError::QueryTimeout => {
                    // Retry transient errors
                    query_database(id).map_err(|e| e.into_dynamic())
                }
                DatabaseError::ConstraintViolation { .. } => {
                    // Don't retry permanent errors
                    Err(report.into_dynamic())
                }
            }
        }
    }
}

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

Quick Start

Add this to your Cargo.toml:

[dependencies]
rootcause = "0.12.1"

Use Report as your error type:

use rootcause::prelude::*;

fn your_function() -> Result<(), Report> {
    // Your existing code with ? already works
    std::fs::read_to_string("/path/to/file")?;
    Ok(())
}

That's it! The ? operator automatically converts any error type to Report.

Ready to learn more? See examples/basic.rs for a hands-on tutorial covering .context(), .attach(), and composing error chains.

Ecosystem

rootcause is designed to be lightweight and extensible. The core library provides essential error handling functionality, while optional companion crates add specialized capabilities:

Related Skills

View on GitHub
GitHub Stars350
CategoryProduct
Updated2d ago
Forks14

Languages

Rust

Security Score

95/100

Audited on Apr 7, 2026

No findings