error-rail 0.9.2

ErrorRail is a no_std compatible Error Handling library for the Rust language.
Documentation

error-rail

Crates.io Docs License Ask DeepWiki

Composable, lazy-evaluated error handling for Rust.

std::error defines error types. error-rail defines how errors flow.

use error_rail::simple::*;

fn load_config() -> BoxedResult<String, std::io::Error> {
    std::fs::read_to_string("config.toml")
        .ctx("loading configuration")
}

Features

  • Lazy formatting — Use context! / .ctx_with(...) to format strings only when errors occur
  • Chainable context — Build rich error traces with .ctx()
  • Validation accumulation — Collect all errors, not just the first
  • Transient error classification — Built-in retry support
  • Error fingerprinting — Deduplicate errors in monitoring systems
  • Async-first — Full async/await support with Tower & Tracing integration
  • no_std compatible — Works in embedded and web environments

Quick Start

cargo add error-rail

For beginners — Start with simple:

use error_rail::simple::*;

fn read_config() -> BoxedResult<String, std::io::Error> {
    std::fs::read_to_string("config.toml")
        .ctx("loading configuration")
}

fn main() {
    if let Err(e) = read_config() {
        eprintln!("{}", e.error_chain());
        // loading configuration -> No such file or directory
    }
}

For general use — Use prelude:

use error_rail::prelude::*;

fn process() -> BoxedResult<String, std::io::Error> {
    let config = std::fs::read_to_string("config.toml")
        .ctx("loading configuration")?;
    Ok(config)
}

API Levels (You do NOT need to learn all of these)

Start here → simple
When you need more → prelude
Only if you are building services → intermediate
Almost never → advanced

Module When to Use What's Included
simple First time using error-rail BoxedResult, rail!, .ctx(), .error_chain()
prelude When you need structured context + context!, group!, ErrorPipeline
intermediate Building services + TransientError, Fingerprint, formatting
advanced Writing libraries + internal builders, ErrorVec
prelude_async Async code + AsyncErrorPipeline, retry, timeout

Core Concepts (Advanced — skip on first read)

If you are using simple, you can skip this entire section.

Context Methods

// Static context (zero allocation)
result.ctx("database connection failed")

// Lazy formatted context (evaluated only on error)
result.ctx(context!("user {} not found", user_id))

// NOTE: `result.ctx(format!(...))` is eager because `format!` runs before `.ctx()`.

// Structured context with tags & metadata
result.ctx(group!(
    tag("database"),
    metadata("query_time_ms", "150")
))

Validation (Collect All Errors)

Note: This is available in error_rail::validation, not in simple.

use error_rail::validation::Validation;

let results: Validation<&str, Vec<_>> = vec![
    validate_age(-5),
    validate_name(""),
].into_iter().collect();

// Both errors collected, not just the first

Macros

use error_rail::prelude::*;

// rail! - Wrap any Result in ErrorPipeline and box it
let result = rail!(std::fs::read_to_string("config.toml"));

// context! - Lazy formatted context (only evaluated on error)
result.ctx(context!("loading config for user {}", user_id))

// group! - Structured context with tags & metadata
result.ctx(group!(tag("config"), metadata("path", "config.toml")))
use error_rail::prelude_async::*;

// rail_async! - Async version of rail!
async fn load() -> BoxedResult<String, IoError> {
    rail_async!(tokio::fs::read_to_string("config.toml"))
        .with_context("loading config")
        .finish_boxed()
        .await
}

Async Support

use error_rail::prelude_async::*;

async fn fetch_user(id: u64) -> BoxedResult<User, DbError> {
    database.get_user(id)
        .ctx("fetching user")
        .await
        .map_err(Box::new)
}

Anti-Patterns

// ❌ DON'T: Chain .ctx() multiple times
fn bad() -> BoxedResult<(), &'static str> {
    Err("original error").ctx("a").ctx("b").ctx("c")  // Noise, not value
}

// ✅ DO: One .ctx() per I/O boundary
fn good() -> BoxedResult<String, std::io::Error> {
    let data = std::fs::read_to_string("file.txt").ctx("reading input")?;
    Ok(data)
}

Avoid Glob Imports in Large Projects

For better IDE support and compile times in large codebases:

// ❌ Glob import (okay for small projects)
use error_rail::prelude::*;

// ✅ Explicit imports (recommended for large projects)
use error_rail::prelude::{BoxedResult, ResultExt, rail};

When should I move from simple to prelude?

Move to prelude when you need:

  • Structured context - tags and metadata for better error categorization
  • Lazy formatted messages - use context! / .ctx_with(...) so formatting only happens on error
  • ErrorPipeline - for building libraries or complex error chains
  • Writing a library - not just an application

You can stay with simple for a long time! It's designed to be sufficient for most applications.

When NOT to Use error-rail

  • Simple scripts that just print errors and exit
  • Teams with little Rust experience
  • When anyhow or eyre already meets your needs

Feature Flags

[dependencies]
error-rail = "0.9"                                    # Core (no_std)
error-rail = { version = "0.9", features = ["std"] }  # + backtraces
error-rail = { version = "0.9", features = ["serde"] } # + serde support
error-rail = { version = "0.9", features = ["async"] } # + async support
error-rail = { version = "0.9", features = ["tokio"] } # + retry, timeout
error-rail = { version = "0.9", features = ["tower"] } # + Tower middleware
error-rail = { version = "0.9", features = ["full"] }  # Everything

Documentation

Resource Description
Quick Start Step-by-step tutorial
Async Guide Async patterns
Patterns Real-world examples
Benchmarks Performance analysis
API Docs Full API reference

Examples

cargo run --example quick_start
cargo run --example async_api_patterns --features tokio
cargo run --example async_tower_integration --features tower

License

Apache-2.0. See LICENSE and NOTICE.