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 erroranyhow::Result<T>— Alias forResult<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
thiserrorfor library errors - Error chains show the full path of failures
- Great for CLI tools and web applications
- Avoid in library public APIs
