How do I work with Anyhow for Application Error Handling in Rust?

Walkthrough

Anyhow is a crate for idiomatic error handling in Rust applications. It provides anyhow::Error, a trait object-based error type that can hold any error, along with utilities for adding context to errors. Unlike thiserror, which is designed for library code with specific error types, anyhow is designed for application code where you want to easily propagate and annotate errors.

Key concepts:

  • anyhow::Error — A trait object that can hold any error
  • anyhow::Result<T> — Alias for Result<T, anyhow::Error>
  • .context() — Add contextual information to errors
  • .with_context() — Add context with closure for lazy evaluation
  • Error chaining — Automatically track error sources
  • Backtraces — Optional capture of stack traces

When to use Anyhow:

  • Application code (binaries, not libraries)
  • When you don't need specific error types
  • When you want easy error propagation
  • When you want to add context to errors

When NOT to use Anyhow:

  • Library code (use thiserror)
  • When callers need to match on specific errors
  • When you need structured error information

Code Examples

Basic Error Propagation

use anyhow::{Result, Context};
use std::fs;
 
fn read_config() -> Result<String> {
    let content = fs::read_to_string("config.toml")?;
    Ok(content)
}
 
fn main() -> Result<()> {
    let config = read_config()?;
    println!("Config: {}", config);
    Ok(())
}

Adding Context

use anyhow::{Result, Context};
use std::fs;
 
fn read_config() -> Result<String> {
    let content = fs::read_to_string("config.toml")
        .context("Failed to read configuration file")?;
    Ok(content)
}
 
fn load_database() -> Result<()> {
    let config = read_config()
        .context("Failed to load database configuration")?;
    println!("Loaded: {}", config);
    Ok(())
}
 
fn main() -> Result<()> {
    load_database()?;
    Ok(())
}

Context with Details

use anyhow::{Result, Context};
use std::fs;
 
fn read_user_file(user_id: u64) -> Result<String> {
    let path = format!("users/{}.json", user_id);
    let content = fs::read_to_string(&path)
        .with_context(|| format!("Failed to read user file at '{}'", path))?;
    Ok(content)
}
 
fn main() -> Result<()> {
    let content = read_user_file(42)?;
    println!("Content: {}", content);
    Ok(())
}

Multiple Error Types

use anyhow::{Result, Context};
use std::fs;
use std::net::TcpStream;
 
fn connect_and_read() -> Result<String> {
    let mut stream = TcpStream::connect("127.0.0.1:8080")
        .context("Failed to connect to server")?;
    
    let config = fs::read_to_string("config.toml")
        .context("Failed to read config")?;
    
    Ok(config)
}
 
fn main() -> Result<()> {
    let data = connect_and_read()?;
    println!("Data: {}", data);
    Ok(())
}

Parsing with Context

use anyhow::{Result, Context};
 
fn parse_number(input: &str) -> Result<i32> {
    input
        .parse::<i32>()
        .with_context(|| format!("Failed to parse '{}' as integer", input))
}
 
fn parse_config_value(key: &str, value: &str) -> Result<i32> {
    parse_number(value)
        .with_context(|| format!("Invalid value for key '{}'", key))
}
 
fn main() -> Result<()> {
    let value = parse_config_value("timeout", "not_a_number")?;
    println!("Value: {}", value);
    Ok(())
}

Error Messages

use anyhow::{Result, anyhow, bail};
 
fn check_age(age: i32) -> Result<()> {
    if age < 0 {
        bail!("Age cannot be negative: {}", age);
    }
    if age > 150 {
        bail!("Age seems unrealistic: {}", age);
    }
    Ok(())
}
 
fn validate_user(name: &str, age: i32) -> Result<()> {
    if name.is_empty() {
        return Err(anyhow!("Name cannot be empty"));
    }
    
    check_age(age)?;
    
    println!("User {} is valid", name);
    Ok(())
}
 
fn main() -> Result<()> {
    validate_user("Alice", 30)?;
    validate_user("", 25)?;  // Will error
    Ok(())
}

Ensuring Values

use anyhow::{Result, ensure};
 
fn process_file(path: &str) -> Result<()> {
    let content = std::fs::read_to_string(path)?;
    
    ensure!(!content.is_empty(), "File is empty");
    ensure!(content.len() < 1000, "File too large: {} bytes", content.len());
    
    println!("Processing: {}", content);
    Ok(())
}
 
fn divide(a: i32, b: i32) -> Result<i32> {
    ensure!(b != 0, "Division by zero");
    Ok(a / b)
}
 
fn main() -> Result<()> {
    process_file("test.txt")?;
    let result = divide(10, 2)?;
    println!("Result: {}", result);
    Ok(())
}

Error Chains

use anyhow::{Result, Context};
 
fn read_config() -> Result<String> {
    std::fs::read_to_string("config.toml")
        .context("Failed to read config file")
}
 
fn parse_config(content: &str) -> Result<i32> {
    content
        .parse()
        .context("Failed to parse timeout value")
}
 
fn get_timeout() -> Result<i32> {
    let content = read_config()
        .context("Failed to load configuration")?;
    
    let timeout = parse_config(&content)
        .context("Failed to parse configuration")?;
    
    Ok(timeout)
}
 
fn main() -> Result<()> {
    match get_timeout() {
        Ok(timeout) => println!("Timeout: {}", timeout),
        Err(e) => {
            // Print full error chain
            for cause in e.chain() {
                eprintln!("Caused by: {}", cause);
            }
        }
    }
    Ok(())
}

Custom Error Types with Thiserror

use anyhow::{Result, Context};
use thiserror::Error;
 
#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("missing field: {0}")]
    MissingField(String),
    
    #[error("invalid value: {0}")]
    InvalidValue(String),
}
 
fn load_config() -> Result<String> {
    // Can propagate thiserror types directly
    Err(ConfigError::MissingField("database".into()))?
}
 
fn main() -> Result<()> {
    let config = load_config()
        .context("Failed to initialize application")?;
    println!("Config: {}", config);
    Ok(())
}

Converting to Anyhow Error

use anyhow::{Error, Result};
 
fn convert_error() -> Result<()> {
    // From std::io::Error
    let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
    let anyhow_err: Error = io_err.into();
    
    // From string
    let str_err: Error = "something went wrong".into();
    
    // Using anyhow! macro
    let msg_err = anyhow!("custom error with {}", "formatting");
    
    Err(msg_err)
}
 
fn main() {
    match convert_error() {
        Ok(()) => println!("Success"),
        Err(e) => println!("Error: {}", e),
    }
}

Downcasting Errors

use anyhow::{Result, anyhow};
use std::io;
 
fn try_operation() -> Result<()> {
    Err(io::Error::new(io::ErrorKind::NotFound, "file missing").into())
}
 
fn main() -> Result<()> {
    let result = try_operation();
    
    if let Err(e) = result {
        // Check if it's a specific error type
        if let Some(io_err) = e.downcast_ref::<io::Error>() {
            println!("IO Error: {}", io_err);
            if io_err.kind() == io::ErrorKind::NotFound {
                println!("File was not found!");
            }
        }
    }
    
    Ok(())
}

Optional Values

use anyhow::{Result, Context};
 
fn find_user(id: u64) -> Result<String> {
    let users = vec![(1, "Alice"), (2, "Bob")];
    
    users
        .into_iter()
        .find(|(uid, _)| *uid == id)
        .map(|(_, name)| name.to_string())
        .ok_or_else(|| anyhow::anyhow!("User {} not found", id))
}
 
fn main() -> Result<()> {
    let user = find_user(1)?;
    println!("Found: {}", user);
    
    let missing = find_user(99);
    assert!(missing.is_err());
    
    Ok(())
}

Error Formatting Options

use anyhow::{Result, Context};
 
fn operation() -> Result<()> {
    std::fs::read_to_string("missing.txt")
        .context("Failed during operation")?;
    Ok(())
}
 
fn main() -> Result<()> {
    if let Err(e) = operation() {
        // Simple format
        println!("Error: {}", e);
        
        // Debug format (includes chain)
        println!("Debug: {:?}", e);
        
        // Pretty debug format
        println!("Pretty: {:#?}", e);
        
        // Manual chain iteration
        println!("\nError chain:");
        for (i, cause) in e.chain().enumerate() {
            println!("  {}: {}", i, cause);
        }
    }
    Ok(())
}

Application Entry Point

use anyhow::{Result, Context};
use std::fs;
 
fn load_config() -> Result<String> {
    fs::read_to_string("app.toml")
        .context("Failed to read app.toml")
}
 
fn initialize_database(config: &str) -> Result<()> {
    // Simulate initialization
    if config.is_empty() {
        anyhow::bail!("Empty configuration");
    }
    println!("Database initialized with: {}", config);
    Ok(())
}
 
fn run_application() -> Result<()> {
    let config = load_config()
        .context("Failed to load configuration")?;
    
    initialize_database(&config)
        .context("Failed to initialize database")?;
    
    println!("Application running");
    Ok(())
}
 
fn main() {
    if let Err(e) = run_application() {
        eprintln!("Error: {:?}", e);
        std::process::exit(1);
    }
}

Testing Error Conditions

use anyhow::{Result, Context, ensure};
 
fn divide(a: f64, b: f64) -> Result<f64> {
    ensure!(b != 0.0, "Division by zero");
    Ok(a / b)
}
 
fn parse_positive(input: &str) -> Result<i32> {
    let value: i32 = input.parse()
        .with_context(|| format!("Failed to parse '{}'", input))?;
    ensure!(value > 0, "Value must be positive, got {}", value);
    Ok(value)
}
 
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_divide_success() {
        assert!(divide(10.0, 2.0).is_ok());
        assert_eq!(divide(10.0, 2.0).unwrap(), 5.0);
    }
    
    #[test]
    fn test_divide_by_zero() {
        let result = divide(10.0, 0.0);
        assert!(result.is_err());
    }
    
    #[test]
    fn test_parse_positive() {
        assert!(parse_positive("42").is_ok());
        assert!(parse_positive("-1").is_err());
        assert!(parse_positive("not a number").is_err());
    }
}
 
fn main() -> Result<()> {
    let result = divide(10.0, 2.0)?;
    println!("Result: {}", result);
    Ok(())
}

HTTP Client Error Handling

use anyhow::{Result, Context};
 
#[derive(Debug)]
pub struct HttpResponse {
    status: u16,
    body: String,
}
 
fn fetch_user(id: u64) -> Result<HttpResponse> {
    // Simulated HTTP response
    if id == 0 {
        return Err(anyhow::anyhow!("Invalid user ID"));
    }
    
    Ok(HttpResponse {
        status: 200,
        body: format!("{{\"id\": {}, \"name\": \"User\"}}", id),
    })
}
 
fn get_user_name(id: u64) -> Result<String> {
    let response = fetch_user(id)
        .with_context(|| format!("Failed to fetch user {}", id))?;
    
    anyhow::ensure!(
        response.status == 200,
        "Unexpected status: {}",
        response.status
    );
    
    Ok(response.body)
}
 
fn main() -> Result<()> {
    let name = get_user_name(1)?;
    println!("User data: {}", name);
    Ok(())
}

Early Return Patterns

use anyhow::{Result, Context, bail};
 
enum UserRole {
    Admin,
    User,
    Guest,
}
 
struct Request {
    user_id: Option<u64>,
    role: Option<UserRole>,
}
 
fn handle_request(req: Request) -> Result<String> {
    let user_id = req.user_id
        .ok_or_else(|| anyhow::anyhow!("Missing user_id"))?;
    
    let role = req.role.ok_or_else(|| anyhow::anyhow!("Missing role"))?;
    
    match role {
        UserRole::Admin => Ok(format!("Admin {} handling", user_id)),
        UserRole::User => Ok(format!("User {} handling", user_id)),
        UserRole::Guest => {
            bail!("Guests cannot handle requests");
        }
    }
}
 
fn main() -> Result<()> {
    let req = Request {
        user_id: Some(1),
        role: Some(UserRole::Admin),
    };
    
    let result = handle_request(req)?;
    println!("Result: {}", result);
    Ok(())
}

Summary

Anyhow Key Imports:

use anyhow::{Result, Context, anyhow, bail, ensure};

Core Types:

Type Description
anyhow::Error Trait object error type
anyhow::Result<T> Result<T, anyhow::Error>

Key Macros:

Macro Description
anyhow! Create an error with formatting
bail! Return early with an error
ensure! Assert condition or return error

Context Methods:

Method Description
.context(msg) Add context to error
.with_context(|| msg) Add lazy context
.chain() Iterate error causes

Error Creation:

Pattern Example
From string anyhow!("error message")
With format anyhow!("error: {}", value)
Early return bail!("fatal error")
Assertion ensure!(x > 0, "must be positive")

Anyhow vs Thiserror:

Feature Anyhow Thiserror
Use case Applications Libraries
Error type Generic Specific
Context Yes No
Matching Limited Full
Boilerplate Minimal More

Key Points:

  • Use anyhow::Result<T> for application error handling
  • Add context with .context() for better error messages
  • Use bail! for early returns with errors
  • Use ensure! for conditional error returns
  • Integrates seamlessly with thiserror for library errors
  • Error chains show the full path of failures
  • Great for CLI tools and web applications
  • Avoid in library public APIs