Loading page…
Rust walkthroughs
Loading page…
anyhow::Context and thiserror::Error for enriching error information?anyhow::Context adds contextual information to errors at propagation time through the ? operator, creating a chain of error messages that explain what operation failed and why. thiserror::Error derives structured error types with the #[source] attribute to capture underlying causes as typed fields, enabling programmatic access to error sources. The key difference: Context is for ad-hoc human-readable context added during error propagation (library/application boundary), while thiserror::Error is for defining typed error enums with structured source relationships (library API design).
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.context(format!("Failed to read config from {}", path))
}
fn parse_config(content: &str) -> Result<Config> {
toml::from_str(content)
.context("Failed to parse config TOML")
}
fn load_config(path: &str) -> Result<Config> {
let content = read_config(path)
.context("Could not load configuration")?;
parse_config(&content)
.context("Invalid configuration format")
}
fn main() -> Result<()> {
let config = load_config("config.toml")?;
Ok(())
}
// Error chain when config file doesn't exist:
// Error: Could not load configuration
// Caused by:
// 0: Failed to read config from config.toml
// 1: No such file or directory (os error 2)Context wraps errors with descriptive messages, building a causal chain that explains the failure path.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Failed to read config file: {path}")]
ReadFailed {
path: String,
#[source]
source: std::io::Error,
},
#[error("Invalid config format")]
ParseFailed {
#[source]
source: toml::de::Error,
},
}
impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
ConfigError::ReadFailed {
path: String::new(),
source: err,
}
}
}
// The #[source] attribute marks which field holds the underlying error
// This allows programmatic access via std::error::Error::source()thiserror::Error defines structured error types where source relationships are explicit fields.
// anyhow::Context: Ad-hoc context added at call site
use anyhow::Context;
fn process_file(path: &str) -> anyhow::Result<()> {
let content = std::fs::read_to_string(path)
.context(format!("Reading {}", path))?;
let data: Data = serde_json::from_str(&content)
.context("Parsing JSON")?;
validate(&data)
.context("Validating data")?;
Ok(())
}
// thiserror::Error: Pre-defined error structure
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProcessError {
#[error("Failed to read {path}")]
ReadError {
path: String,
#[source]
source: std::io::Error,
},
#[error("Failed to parse JSON")]
ParseError {
#[source]
source: serde_json::Error,
},
#[error("Validation failed")]
ValidationError {
#[source]
source: ValidationError,
},
}
fn process_file(path: &str) -> Result<(), ProcessError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ProcessError::ReadError {
path: path.to_string(),
source: e,
})?;
let data: Data = serde_json::from_str(&content)
.map_err(ProcessError::ParseError)?;
validate(&data)
.map_err(ProcessError::ValidationError)?;
Ok(())
}Context adds context dynamically at each error site; thiserror requires defining error variants upfront.
use anyhow::{Context, Result};
fn main() -> Result<()> {
// Context is designed to work with ? operator
// It wraps the error before propagating
let file = std::fs::File::open("data.txt")
.context("Opening data file")?; // Wraps io::Error
let content = std::fs::read_to_string("data.txt")
.context("Reading file content")?;
let config: Config = toml::from_str(&content)
.context("Parsing TOML config")?;
// Each context creates a layer in the error chain
Ok(())
}
// If the file doesn't exist:
// Error: Opening data file
// Caused by:
// 0: Reading file content
// 1: No such file or directory (os error 2)The ? operator combined with .context() builds a stack trace of what went wrong.
use thiserror::Error;
use std::error::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("Database connection failed")]
DatabaseError {
#[source]
source: sql::Error,
},
#[error("User not found: {id}")]
UserNotFound {
id: u64,
#[source]
source: DatabaseError,
},
}
fn main() {
let error: Box<dyn Error> = AppError::UserNotFound {
id: 42,
source: AppError::DatabaseError {
source: sql::Error::ConnectionFailed,
},
};
// Programmatic access to source chain
let mut current: Option<&dyn Error> = Some(error.as_ref());
while let Some(err) = current {
println!("Error: {}", err);
current = err.source();
}
// Output:
// Error: User not found: 42
// Error: Database connection failed
// Error: connection failed
}
// This enables:
// - Error matching on specific types
// - Structured logging/monitoring
// - Conditional error handlingthiserror enables traversing the error chain programmatically via std::error::Error::source().
use anyhow::{Context, Result};
// anyhow is ideal for applications where you want rich error messages
// without defining custom error types
fn run_server() -> Result<()> {
let config = load_config("config.toml")
.context("Failed to initialize server configuration")?;
let listener = bind_socket(&config.bind_address)
.context("Failed to bind server socket")?;
let database = connect_db(&config.database_url)
.context("Failed to connect to database")?;
serve_requests(listener, database)
.context("Server error")?;
Ok(())
}
// Error output shows the full context chain:
// Error: Server error
// Caused by:
// 0: Failed to connect to database
// 1: Connection refused (os error 111)anyhow::Context excels in application code where context messages help debug issues.
use thiserror::Error;
// thiserror is ideal for libraries where consumers need to match on errors
#[derive(Debug, Error)]
pub enum DatabaseError {
#[error("Connection failed to {host}:{port}")]
ConnectionFailed {
host: String,
port: u16,
#[source]
source: std::io::Error,
},
#[error("Query execution failed")]
QueryError {
#[source]
source: sql::Error,
},
#[error("Transaction rolled back")]
TransactionError {
#[source]
source: Box<Self>,
},
#[error("Pool exhausted: no available connections")]
PoolExhausted,
}
// Consumers can match on specific error variants:
fn handle_db_error(err: DatabaseError) {
match err {
DatabaseError::ConnectionFailed { host, port, .. } => {
println!("Retry connection to {}:{}", host, port);
}
DatabaseError::PoolExhausted => {
println!("Wait for available connection");
}
_ => {
println!("Other database error: {}", err);
}
}
}thiserror defines typed error APIs that library consumers can match on.
use anyhow::{Context, Result};
use thiserror::Error;
// Library defines typed errors with thiserror
#[derive(Debug, Error)]
pub enum ParseError {
#[error("Invalid syntax at line {line}")]
SyntaxError { line: usize },
#[error("Unexpected token: expected {expected}, got {actual}")]
UnexpectedToken { expected: String, actual: String },
#[error("IO error")]
Io {
#[source]
source: std::io::Error,
},
}
// Application adds context with anyhow
fn parse_config_file(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.context(format!("Reading config file: {}", path))
.map_err(|e| ParseError::Io { source: e.into() })?;
let config = parse_config(&content)
.context("Parsing configuration")
.map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;
Ok(config)
}Use thiserror for library error types, anyhow::Context in application code.
use anyhow::{anyhow, Context, Result};
fn validate_age(age: i32) -> Result<()> {
if age < 0 {
return Err(anyhow!("Age cannot be negative: {}", age));
}
if age > 150 {
return Err(anyhow!("Age seems unrealistic: {}", age));
}
Ok(())
}
fn process_user(name: &str, age: i32) -> Result<()> {
validate_age(age)
.context(format!("Validating age for user {}", name))?;
Ok(())
}
fn main() -> Result<()> {
process_user("Alice", -5)?;
Ok(())
}
// Error output:
// Error: Validating age for user Alice
// Caused by:
// 0: Age cannot be negative: -5Combine anyhow! for creating errors and .context() for adding propagation context.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("HTTP request failed")]
Http {
#[source]
source: reqwest::Error,
},
#[error("Serialization failed")]
Serialization {
#[source]
source: serde_json::Error,
},
#[error("Business logic error: {message}")]
Business {
message: String,
// source is optional - not all errors have underlying causes
},
}
// #[source] creates a causal chain accessible via Error::source()
// This is different from a backtrace (call stack)
// The source chain is about *what* caused the error, not *where*
// anyhow adds both context (what) and can include backtraces (where)
// when RUST_BACKTRACE=1 is set#[source] defines causal relationships; anyhow::Context provides both context and optional backtraces.
use anyhow::{Context, Result};
use thiserror::Error;
// anyhow error display:
fn test_anyhow() -> Result<()> {
std::fs::read_to_string("nonexistent.txt")
.context("Loading file")?;
Ok(())
}
// Display: "Loading file"
// Debug: Full chain with "Caused by:" sections
// (anyhow::Error implements both Display and Debug differently)
// thiserror error display:
#[derive(Debug, Error)]
pub enum MyError {
#[error("File not found: {path}")]
FileNotFound { path: String },
}
// Display: Uses #[error(...)] attribute format
// Debug: Uses derive Debug format (or can be customized)
// You control the exact message format with #[error("...")]
// anyhow: Less control, automatic context chaining
// thiserror: Full control over error message formatanyhow generates context chains automatically; thiserror gives you explicit control over error messages.
use thiserror::Error;
use std::error::Error;
use std::io;
#[derive(Debug, Error)]
pub enum MyError {
#[error("IO error")]
Io(#[source] io::Error),
#[error("Parse error")]
Parse(String),
}
fn handle_error(err: Box<dyn Error>) {
// With thiserror, you can downcast to match specific types
if let Some(my_err) = err.downcast_ref::<MyError>() {
match my_err {
MyError::Io(e) => println!("IO error: {}", e),
MyError::Parse(s) => println!("Parse error: {}", s),
}
}
// You can also access the source chain
if let Some(source) = err.source() {
println!("Caused by: {}", source);
}
}
// anyhow also supports downcasting
use anyhow::anyhow;
fn handle_anyhow_error(err: anyhow::Error) {
if let Some(io_err) = err.downcast_ref::<io::Error>() {
println!("Got IO error: {}", io_err);
}
}Both support downcasting; thiserror makes the source relationship explicit via #[source].
use anyhow::Context;
use thiserror::Error;
// anyhow::Error:
// - Heap-allocated (dyn Error inside)
// - Can hold any error type
// - Contains optional backtrace
// - Slightly larger memory footprint
// - Great for propagating diverse errors
// thiserror-defined errors:
// - Stack-allocated (enum variants)
// - Size depends on variant data
// - No backtrace overhead
// - Predictable size for matching
// - Better for libraries with specific error types
// Example size comparison:
#[derive(Debug, Error)]
pub enum LibraryError {
#[error("IO error")]
Io(#[source] std::io::Error), // Size: std::io::Error + enum discriminant
#[error("Invalid input")]
InvalidInput, // Size: just discriminant (small)
}
// anyhow::Error can hold any of these plus context strings
// Size overhead: Box<dyn Error> + Option<Backtrace> + context stringsanyhow::Error is heap-allocated and can hold any error; thiserror errors are sized at compile time.
// Application code: Use anyhow
use anyhow::{Context, Result};
fn run_application() -> Result<()> {
let config = load_config()
.context("Failed to load configuration")?;
let server = start_server(&config)
.context("Failed to start server")?;
serve_requests(server)
.context("Server error")?;
Ok(())
}
// Library code: Use thiserror
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("IO error reading {path}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("Invalid format")]
Format {
#[source]
source: toml::de::Error,
},
}
pub fn load_config(path: &str) -> Result<Config, ConfigError> {
// Library returns typed errors
}
// Application consuming library:
fn app_load_config(path: &str) -> anyhow::Result<Config> {
load_config(path)
.context(format!("Loading config from {}", path))
.map_err(anyhow::Error::from)
}Library: Use thiserror for typed error APIs that consumers can match on.
Application: Use anyhow::Context to add rich context for debugging.
use anyhow::{Context, Result};
fn main() -> Result<()> {
// Context works on Option too (not just Result)
let value: Option<i32> = None;
let value = value
.context("Expected a value but got None")?;
// This converts None to an error with the context message
// Useful for:
// - HashMap lookups
// - Optional configuration
// - Finding items
let config: HashMap<&str, &str> = HashMap::new();
let db_url = config
.get("database_url")
.context("Missing 'database_url' in configuration")?;
Ok(())
}
// Error: Missing 'database_url' in configuration.context() works on Option<T> to convert None into an error with a message.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MyError {
#[error("Operation failed")]
Failed {
#[source]
source: std::io::Error,
#[backtrace] // Requires nightly or feature flag
backtrace: std::backtrace::Backtrace,
},
}
// Note: Backtrace in thiserror requires std::backtrace support
// This is separate from the source chain
// anyhow::Context automatically captures backtraces when RUST_BACKTRACE=1thiserror supports #[backtrace] for explicit backtrace capture; anyhow does this automatically.
anyhow::Context:
.context()? operator for ergonomic error propagationanyhow::Error (boxed trait object)thiserror::Error:
#[derive(Error)]#[source] attribute marks underlying error fieldsError::source()Key comparison:
| Aspect | anyhow::Context | thiserror::Error |
|--------|-----------------|------------------|
| When to add context | Runtime (propagation) | Compile time (definition) |
| Error type | anyhow::Error (boxed) | User-defined (sized) |
| Source access | Via Error::source() | Via #[source] field |
| Message control | Automatic chaining | Explicit via #[error(...)] |
| Downcasting | Supported | Supported (with enum matching) |
| Best for | Applications | Libraries |
The fundamental insight: anyhow::Context is about enriching errors with context during propagation—you add information about what you were trying to do when an error occurred. thiserror::Error is about defining structured error types with explicit source relationships—you declare the shape of your errors upfront. Use thiserror when you want consumers to match on specific error variants; use anyhow::Context when you want rich error messages without defining error types.