Rootcause
A flexible, ergonomic, and inspectable error reporting library for Rust.
Install / Use
/learn @rootcause-rs/RootcauseREADME
rootcause
A flexible, ergonomic, and inspectable error reporting library for Rust.
<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
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
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:
rootcause-backtrace- Automatic stack trace capture
Related Skills
next
A beautifully designed, floating Pomodoro timer that respects your workspace.
product-manager-skills
50PM skill for Claude Code, Codex, Cursor, and Windsurf: diagnose SaaS metrics, critique PRDs, plan roadmaps, run discovery, and coach PM career transitions.
devplan-mcp-server
3MCP server for generating development plans, project roadmaps, and task breakdowns for Claude Code. Turn project ideas into paint-by-numbers implementation plans.
