Loading pageā¦
Rust walkthroughs
Loading pageā¦
anyhow::Error::new wrap custom error types while preserving their std::error::Error implementation?anyhow::Error::new wraps any type implementing std::error::Error plus Send + Sync + 'static into a heap-allocated ErrorImpl that stores the error alongside a vtable for dynamic dispatch, preserving access to the original error's methods through type-erased interfaces. The wrapped error can be downcast back to its original type, have its source() chain traversed, and be formatted with all its original details. This works because anyhow::Error internally stores a Box<ErrorImpl<E>> where E is the concrete error type, and the vtable provides methods like source(), downcast(), and fmt::Display that delegate to the wrapped type's implementations.
use anyhow::{Error, Result};
// Define a custom error type
#[derive(Debug)]
struct DatabaseError {
message: String,
}
impl std::fmt::Display for DatabaseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Database error: {}", self.message)
}
}
impl std::error::Error for DatabaseError {}
fn basic_wrapping() {
// Wrap a custom error type
let db_error = DatabaseError {
message: "Connection failed".to_string(),
};
// anyhow::Error::new wraps the error
let anyhow_error = Error::new(db_error);
// Display works correctly
println!("Error: {}", anyhow_error);
// Output: Error: Database error: Connection failed
// The wrapped error's Display implementation is preserved
}Error::new takes any error type and wraps it, preserving its Display implementation.
use anyhow::Error;
// Error::new requires: std::error::Error + Send + Sync + 'static
#[derive(Debug)]
struct MyError {
code: u32,
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error code: {}", self.code)
}
}
impl std::error::Error for MyError {}
fn type_requirements() {
// This works: MyError implements Error + Send + Sync + 'static
let error = Error::new(MyError { code: 404 });
// These would NOT compile:
// - Types without std::error::Error impl
// - Types that are !Send or !Sync
// - Types with non-'static references
// Example of what's NOT allowed:
// struct RefError<'a>(&'a str); // Not 'static
// impl std::error::Error for RefError<'_> {}
// let e = Error::new(RefError(&s)); // Won't compile: needs 'static
}The wrapped type must implement std::error::Error, Send, Sync, and 'static.
use anyhow::Error;
// Simplified view of anyhow's internal structure:
//
// pub struct Error {
// inner: Box<ErrorImpl<dyn StdError>>,
// }
//
// Where ErrorImpl<E> stores:
// - The error object: E (the actual error type)
// - A vtable for dynamic dispatch
#[derive(Debug)]
struct IoError {
message: String,
}
impl std::fmt::Display for IoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "IO failed: {}", self.message)
}
}
impl std::error::Error for IoError {}
fn internal_structure() {
let io_error = IoError {
message: "File not found".to_string(),
};
// Error::new creates Box<ErrorImpl<IoError>>
// This box:
// 1. Allocates on heap (error may be large)
// 2. Stores IoError and its vtable
// 3. Returns fat pointer to dyn StdError
let error: Error = Error::new(io_error);
// The heap allocation allows:
// - Different sized errors to be stored uniformly
// - Error type to be erased (anyhow::Error is one type)
// - Vtable to provide dynamic dispatch
}The error is heap-allocated with a vtable for type-erased dynamic dispatch.
use anyhow::Error;
use std::error::Error as StdError;
#[derive(Debug)]
struct ChainError {
message: String,
source: Option<Box<dyn StdError + Send + Sync>>,
}
impl std::fmt::Display for ChainError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl StdError for ChainError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source.as_ref().map(|e| e.as_ref() as &dyn StdError)
}
}
fn preserved_methods() {
let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let chain_error = ChainError {
message: "Operation failed".to_string(),
source: Some(Box::new(inner)),
};
// Wrap the chain error
let error = Error::new(chain_error);
// source() is preserved and accessible
if let Some(source) = error.source() {
println!("Caused by: {}", source);
}
// The entire error chain is preserved
let mut current: &dyn StdError = &error;
while let Some(cause) = current.source() {
println!("Caused by: {}", cause);
current = cause;
}
}The source() method from std::error::Error is preserved and works through the wrapper.
use anyhow::Error;
#[derive(Debug)]
struct ConfigError {
file: String,
line: u32,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Config error at {}:{}
", self.file, self.line)
}
}
impl std::error::Error for ConfigError {}
fn downcasting() {
let config_error = ConfigError {
file: "config.toml".to_string(),
line: 42,
};
let error: Error = Error::new(config_error);
// Downcast to original type
if let Some(config_err) = error.downcast_ref::<ConfigError>() {
println!("File: {}, Line: {}", config_err.file, config_err.line);
}
// Downcast consuming self
let error2 = Error::new(ConfigError {
file: "app.toml".to_string(),
line: 10,
});
let recovered: ConfigError = error2.downcast::<ConfigError>().unwrap();
println!("Recovered: {} at line {}", recovered.file, recovered.line);
}downcast_ref() and downcast() retrieve the original error type from the wrapper.
use anyhow::Error;
// When Error::new wraps an error, it creates a vtable with:
// - display: fn(&self, &mut Formatter) -> fmt::Result
// - debug: fn(&self, &mut Formatter) -> fmt::Result
// - source: fn(&self) -> Option<&dyn Error>
// - downcast: fn(self: Box<Self>, TypeId) -> Result<Box<dyn Any>, Box<dyn Error>>
// - etc.
#[derive(Debug)]
struct NetworkError {
code: i32,
}
impl std::fmt::Display for NetworkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Network error code {}", self.code)
}
}
impl std::error::Error for NetworkError {}
fn vtable_explanation() {
let network_error = NetworkError { code: 500 };
// Error::new creates vtable pointing to NetworkError's implementations
let error: Error = Error::new(network_error);
// When you call:
// - error.to_string() -> vtable.display()
// - error.source() -> vtable.source()
// - error.downcast_ref() -> vtable.downcast_ref()
// The vtable ensures NetworkError's implementations are called,
// not some generic default
let message = format!("{}", error); // Uses NetworkError's Display
assert_eq!(message, "Network error code 500");
}The vtable delegates calls to the original error type's implementations.
use anyhow::{Context, Error, Result};
#[derive(Debug)]
struct ParseError {
position: usize,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Parse error at position {}", self.position)
}
}
impl std::error::Error for ParseError {}
fn error_chains() -> Result<()> {
// Create a base error
let parse_error = ParseError { position: 42 };
// Wrap and add context
let error = Error::new(parse_error).context("Failed to parse configuration");
// The context creates an error chain:
// 1. "Failed to parse configuration"
// 2. "Parse error at position 42"
// source() traverses the chain
let mut current: &dyn std::error::Error = &error;
println!("Error: {}", current);
while let Some(cause) = current.source() {
println!("Caused by: {}", cause);
current = cause;
}
// Output:
// Error: Failed to parse configuration
// Caused by: Parse error at position 42
Err(error)
}.context() creates error chains that preserve the original error as a source.
use anyhow::Error;
use std::error::Error as StdError;
#[derive(Debug)]
struct AppError {
details: String,
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.details)
}
}
impl StdError for AppError {}
fn comparison_with_box() {
let app_error = AppError {
details: "Something went wrong".to_string(),
};
// Box<dyn Error>: Standard library approach
let boxed: Box<dyn StdError + Send + Sync> = Box::new(app_error);
// anyhow::Error: Enhanced error handling
let anyhow_err = Error::new(AppError {
details: "Something went wrong".to_string(),
});
// Key differences:
// 1. Downcasting
// Box<dyn Error>: needs dynamic type checking
let _ = boxed.downcast_ref::<AppError>(); // Works but verbose
// anyhow::Error: cleaner API
let _ = anyhow_err.downcast_ref::<AppError>();
// 2. Context
// Box<dyn Error>: no built-in context support
// anyhow::Error: .context() for adding information
// 3. Construction
// Box<dyn Error>: requires Box::new()
// anyhow::Error: Error::new() or .into()
}anyhow::Error provides a cleaner API over Box<dyn Error> with additional features.
use anyhow::Error;
use std::error::Error as StdError;
fn error_trait_impl() {
// anyhow::Error implements std::error::Error
// This means it can be used anywhere StdError is expected
let error = Error::msg("Something failed");
// Can be used as &dyn StdError
let std_error: &dyn StdError = &error;
println!("As StdError: {}", std_error);
// Can be used in error chains
let wrapped = Error::new(error).context("While processing");
// source() works through the chain
if let Some(source) = wrapped.source() {
println!("Source: {}", source);
}
}anyhow::Error itself implements std::error::Error, so it integrates with the standard error ecosystem.
use anyhow::{Error, anyhow, bail};
#[derive(Debug)]
struct ValidationError {
field: String,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Validation failed for field '{}'", self.field)
}
}
impl std::error::Error for ValidationError {}
fn creation_methods() {
// Error::new: Wrap a custom error type
let custom_error = Error::new(ValidationError {
field: "email".to_string(),
});
// Error::msg: Create a simple message error
let message_error = Error::msg("Something went wrong");
// anyhow! macro: Format a message error
let formatted_error = anyhow!("Error code: {}", 500);
// bail! macro: Return early with an error
// bail!("Critical failure"); // Returns Err(Error)
// All produce anyhow::Error, but:
// - Error::new preserves custom type (can downcast)
// - Error::msg creates simple message (no custom type)
// With custom error:
if let Some(validation_err) = custom_error.downcast_ref::<ValidationError>() {
println!("Field: {}", validation_err.field);
}
// With message error:
// No custom type to downcast to
}Error::new preserves the custom type; Error::msg creates a simple string error.
use anyhow::Error;
#[derive(Debug)]
struct DetailedError {
code: u32,
message: String,
context: Vec<String>,
}
impl std::fmt::Display for DetailedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error {}: {}", self.code, self.message)
}
}
impl std::error::Error for DetailedError {}
fn debug_preservation() {
let error = DetailedError {
code: 500,
message: "Internal error".to_string(),
context: vec
!["request.json".to_string(), "user_id: 123".to_string()],
};
let wrapped = Error::new(error);
// Display: uses original's Display
println!("Display: {}", wrapped);
// Output: Error 500: Internal error
// Debug: uses original's Debug
println!("Debug: {:?}", wrapped);
// Output: DetailedError { code: 500, message: "Internal error", context: ["request.json", "user_id: 123"] }
// {:?} shows the full Debug output of the wrapped error
}Both Display and Debug implementations are preserved through the wrapper.
use anyhow::{Error, Result};
#[derive(Debug)]
struct AuthError {
user: String,
}
impl std::fmt::Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Authentication failed for user '{}'", self.user)
}
}
impl std::error::Error for AuthError {}
// Manual wrapping with Error::new
fn manual_wrapping() -> Result<()> {
let auth_error = AuthError {
user: "admin".to_string(),
};
Err(Error::new(auth_error))
}
// Automatic conversion with .into()
fn automatic_conversion() -> Result<()> {
let auth_error = AuthError {
user: "admin".to_string(),
};
// Into<anyhow::Error> is implemented for types with std::error::Error
Err(auth_error.into())
}
// Using ? operator (also uses Into)
fn with_question_mark() -> Result<()> {
// Function that returns AuthError
fn authenticate() -> Result<(), AuthError> {
Err(AuthError {
user: "guest".to_string(),
})
}
// ? converts AuthError -> anyhow::Error automatically
authenticate()?;
Ok(())
}The From<E> implementation for anyhow::Error allows automatic conversion.
use anyhow::{Error, Result};
use std::fs::File;
use std::io::Read;
#[derive(Debug)]
struct ConfigError {
message: String,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Config error: {}", self.message)
}
}
impl std::error::Error for ConfigError {}
fn multiple_error_types() -> Result<()> {
// Different error types, all converted to anyhow::Error
// std::io::Error -> anyhow::Error
let mut file = File::open("config.toml")?;
// Custom ConfigError -> anyhow::Error
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| Error::new(e))?;
// Manual conversion
if contents.is_empty() {
return Err(Error::new(ConfigError {
message: "Empty config file".to_string(),
}));
}
Ok(())
}anyhow::Error unifies different error types through automatic conversion.
use anyhow::Error;
use std::error::Error as StdError;
#[derive(Debug)]
struct TimeoutError {
duration_ms: u64,
}
impl std::fmt::Display for TimeoutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Operation timed out after {}ms", self.duration_ms)
}
}
impl StdError for TimeoutError {}
fn accessing_inner() {
let error = Error::new(TimeoutError { duration_ms: 5000 });
// Method 1: downcast_ref (borrow)
if let Some(timeout) = error.downcast_ref::<TimeoutError>() {
println!("Timeout duration: {}ms", timeout.duration_ms);
}
// Method 2: downcast (consume)
let error2 = Error::new(TimeoutError { duration_ms: 3000 });
match error2.downcast::<TimeoutError>() {
Ok(timeout) => println!("Recovered timeout: {}ms", timeout.duration_ms),
Err(original_error) => println!("Not a TimeoutError: {}", original_error),
}
// Method 3: as inner dyn Error
let error3 = Error::new(TimeoutError { duration_ms: 1000 });
let inner: &dyn StdError = error3.inner();
println!("Inner error: {}", inner);
}Multiple methods exist to access the wrapped error depending on your needs.
use anyhow::{Context, Error, Result};
use std::error::Error as StdError;
// Define domain errors
#[derive(Debug)]
pub struct DatabaseError {
pub query: String,
pub reason: String,
}
impl std::fmt::Display for DatabaseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Database error on query '{}': {}", self.query, self.reason)
}
}
impl StdError for DatabaseError {}
#[derive(Debug)]
pub struct ValidationError {
pub field: String,
pub value: String,
pub constraint: String,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Validation failed: field '{}' with value '{}' violates {}",
self.field, self.value, self.constraint
)
}
}
impl StdError for ValidationError {}
// Application function using anyhow
fn process_user(id: u32, name: &str) -> Result<()> {
// Validate input
if name.is_empty() {
return Err(Error::new(ValidationError {
field: "name".to_string(),
value: name.to_string(),
constraint: "non-empty".to_string(),
}));
}
// Database operation with context
fn query_database(id: u32) -> Result<String, DatabaseError> {
Err(DatabaseError {
query: format!("SELECT * FROM users WHERE id = {}", id),
reason: "Connection refused".to_string(),
})
}
// Convert domain error to anyhow with context
let result = query_database(id)
.map_err(Error::new)
.context(format!("Failed to fetch user {}", id))?;
Ok(())
}
fn handle_errors() {
match process_user(1, "") {
Ok(()) => println!("Success"),
Err(e) => {
// Try to downcast to specific error types
if let Some(validation) = e.downcast_ref::<ValidationError>() {
println!("Validation error on field: {}", validation.field);
} else if let Some(db) = e.downcast_ref::<DatabaseError>() {
println!("Database error: {}", db.reason);
} else {
println!("Unknown error: {}", e);
}
// Print full chain
let mut current: &dyn StdError = &*e;
println!("Error chain:");
println!(" - {}", current);
while let Some(cause) = current.source() {
println!(" - {}", cause);
current = cause;
}
}
}
}Real applications use domain-specific errors wrapped in anyhow::Error with context.
How wrapping works:
// Error::new creates:
// 1. Heap allocation for the error
// 2. Vtable pointing to the error's implementations
// 3. Type-erased reference stored in anyhow::Error
let error = Error::new(MyCustomError { ... });
// Equivalent to:
// let boxed: Box<dyn StdError + Send + Sync> = Box::new(MyCustomError { ... });
// But with:
// - Downcasting support
// - Context chaining
// - Cleaner APIKey preservation points:
// What's preserved from the wrapped error:
// 1. Display implementation -> format!("{}", error)
// 2. Debug implementation -> format!("{:?}", error)
// 3. source() method -> error.source()
// 4. Type information -> error.downcast_ref::<T>()
// What's NOT preserved:
// - Direct field access (must downcast)
// - Private methods (can't call on anyhow::Error)
// - Non-error trait implementationsVtable mechanism:
// Internal structure (simplified):
struct ErrorVtable {
display: fn(&dyn Any, &mut Formatter) -> fmt::Result,
debug: fn(&dyn Any, &mut Formatter) -> fmt::Result,
source: fn(&dyn Any) -> Option<&dyn Error>,
downcast: fn(Box<dyn Any>, TypeId) -> Result<Box<T>, Box<dyn Error>>,
}
// When Error::new::<T>(error):
// - Creates vtable with T's implementations
// - Stores T in Box<ErrorImpl<T>>
// - Returns type-erased ErrorKey insight: anyhow::Error::new wraps custom error types by boxing them with a vtable that preserves their std::error::Error implementationāspecifically their Display, Debug, and source() methodsāwhile providing downcasting capabilities to recover the original type. The wrapper itself implements std::error::Error, making it compatible with the standard error ecosystem. This design allows anyhow::Error to unify different error types while still enabling type-specific error handling through downcasting. The heap allocation is necessary because anyhow::Error must be a single type regardless of what's wrapped inside, and the vtable enables dynamic dispatch to the original type's implementations without knowing the concrete type at compile time.