Loading pageā¦
Rust walkthroughs
Loading pageā¦
thiserror::Error::source enable error chain inspection for nested errors?thiserror::Error::source automatically implements the std::error::Error::source method for derived error types, returning the underlying cause stored in fields marked with #[source] or named source. The source method in std::error::Error provides a standard way to traverse error chains, allowing tools to display full context when errors bubble up through multiple layers. When an error wraps another errorālike a database error wrapping an I/O error wrapping a network errorāthe source chain lets you walk from the outermost error down to the root cause. thiserror automates this by detecting source fields and generating the appropriate implementation, but you can also use #[source] to explicitly mark which field contains the underlying error when naming conventions differ.
use std::error::Error;
fn print_error_chain(e: &dyn Error) {
println!("Error: {}", e);
let mut source = e.source();
let mut depth = 1;
while let Some(err) = source {
println!(" Caused by: {}", err);
source = err.source();
depth += 1;
}
println!(" ({} errors in chain)", depth);
}
fn main() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
print_error_chain(&io_error);
}source() returns Option<&dyn Error>, enabling chain traversal.
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
// #[from] implies #[source]
}
fn main() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "config.toml");
let app_err = AppError::Io(io_err);
println!("Top error: {}", app_err);
if let Some(source) = app_err.source() {
println!("Source: {}", source);
}
}#[from] automatically implements both From and source.
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Configuration error")]
Config {
#[source]
cause: std::io::Error,
},
#[error("Network error: {message}")]
Network {
message: String,
#[source]
underlying: Box<dyn Error + Send + Sync>,
},
}
fn main() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing config");
let app_err = AppError::Config { cause: io_err };
println!("Error: {}", app_err);
println!("Source: {}", app_err.source().unwrap());
}Use #[source] to mark which field is the error source when not named source.
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
// Named "source" - automatically detected
#[error("Query failed")]
QueryFailed {
source: sqlx::Error, // Auto-detected as source
},
// Named "cause" - not auto-detected (use #[source])
#[error("Connection failed")]
ConnectionFailed {
#[source]
cause: sqlx::Error,
},
// #[from] - always implies #[source]
#[error("IO error")]
IoError(#[from] std::io::Error),
}
// Mock sqlx for compilation
mod sqlx {
use std::fmt;
#[derive(Debug)]
pub struct Error;
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "sqlx error")
}
}
impl std::error::Error for Error {}
}Fields named source are auto-detected; other names need #[source].
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Service error")]
Service {
#[source]
inner: ServiceError,
},
}
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Database error")]
Database {
#[source]
inner: DbError,
},
}
#[derive(Error, Debug)]
pub enum DbError {
#[error("Connection refused")]
ConnectionRefused,
}
fn print_full_chain(e: &dyn Error) {
let mut current = Some(e);
let mut level = 0;
while let Some(err) = current {
let indent = " ".repeat(level);
println!("{}{}", indent, err);
current = err.source();
level += 1;
}
}
fn main() {
let db_err = DbError::ConnectionRefused;
let service_err = ServiceError::Database { inner: db_err };
let app_err = AppError::Service { inner: service_err };
println!("Full error chain:");
print_full_chain(&app_err);
}Walking source() reveals the complete error chain from top to bottom.
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Processing failed")]
Processing {
#[source]
cause: Box<dyn Error + Send + Sync>,
},
}
fn create_error() -> AppError {
let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "operation timed out");
AppError::Processing {
cause: Box::new(io_err),
}
}
fn main() {
let err = create_error();
println!("Error: {}", err);
if let Some(source) = err.source() {
println!("Caused by: {}", source);
}
}Boxed errors work with #[source] for dynamic error types.
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum MultiSourceError {
// Only one source per variant
#[error("Failed to read from {path}")]
ReadError {
path: String,
#[source]
error: std::io::Error,
},
// Additional context is not a "source"
#[error("Failed to process {context}")]
ProcessError {
context: String,
#[source]
error: std::io::Error,
},
}
// If you need multiple sources, use a wrapper
#[derive(Error, Debug)]
pub enum WrapperError {
#[error("Multiple failures")]
Multiple {
#[source]
primary: Box<dyn Error>,
// Secondary can be stored but not in source chain
secondary: Box<dyn Error>,
},
}Each variant can have one primary source; store additional errors separately.
use thiserror::Error;
use std::error::Error;
// Custom error with source
#[derive(Error, Debug)]
#[error("Application error")]
pub struct AppError {
#[source]
source: anyhow::Error,
}
fn demonstrate_anyhow_chain() -> Result<(), AppError> {
let inner = anyhow::anyhow!("inner error")
.context("added context");
Err(AppError { source: inner })
}
fn main() {
if let Err(e) = demonstrate_anyhow_chain() {
println!("Top: {}", e);
if let Some(s) = e.source() {
println!("Source: {}", s);
}
}
}anyhow::Error implements Error::source for chain traversal.
use std::error::Error;
use std::fmt;
// Manual implementation
#[derive(Debug)]
pub struct ManualError {
message: String,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl fmt::Display for ManualError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for ManualError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|e| e.as_ref() as &dyn Error)
}
}
// thiserror-derived
#[derive(Error, Debug)]
pub struct AutoError {
#[error("Auto error")]
message: &'static str,
#[source]
cause: Box<dyn Error + Send + Sync>,
}
// thiserror generates the source() implementation automaticallythiserror generates boilerplate that would otherwise be tedious.
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum OperationError<E: Error + 'static> {
#[error("Operation failed")]
Failed {
#[source]
cause: E,
},
}
fn main() {
let err = OperationError::<std::io::Error>::Failed {
cause: std::io::Error::new(std::io::ErrorKind::NotFound, "resource"),
};
println!("Error: {}", err);
if let Some(source) = err.source() {
println!("Source: {}", source);
}
}Generic error types work with #[source].
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum Layer1 {
#[error("Layer 1 error")]
Error1(#[from] Layer2),
}
#[derive(Error, Debug)]
pub enum Layer2 {
#[error("Layer 2 error")]
Error2(#[from] Layer3),
}
#[derive(Error, Debug)]
pub enum Layer3 {
#[error("Root cause: {message}")]
RootCause { message: String },
}
fn print_chain_depth(e: &dyn Error) -> usize {
let mut depth = 0;
let mut current = Some(e);
while let Some(err) = current {
depth += 1;
current = err.source();
}
depth
}
fn main() {
let root = Layer3::RootCause { message: "something went wrong".to_string() };
let l2 = Layer2::Error2(root);
let l1 = Layer1::Error1(l2);
println!("Error chain depth: {}", print_chain_depth(&l1));
// Walk and print
let mut current: &dyn Error = &l1;
loop {
println!("- {}", current);
match current.source() {
Some(s) => current = s,
None => break,
}
}
}source() chains work across multiple nested error types.
use std::error::Error;
use std::fmt;
/// Find the root cause of an error chain
fn find_root_cause(e: &dyn Error) -> &dyn Error {
let mut current = e;
while let Some(source) = current.source() {
current = source;
}
current
}
/// Check if any error in chain matches a type
fn chain_contains<E: Error + 'static>(e: &dyn Error) -> bool {
let mut current = Some(e);
while let Some(err) = current {
if err.downcast_ref::<E>().is_some() {
return true;
}
current = err.source();
}
false
}
/// Format entire error chain
fn format_chain(e: &dyn Error) -> String {
let mut result = e.to_string();
let mut current = e.source();
while let Some(err) = current {
result.push_str("\n Caused by: ");
result.push_str(&err.to_string());
current = err.source();
}
result
}
#[derive(Error, Debug)]
#[error("Top error")]
struct TopError {
#[source]
source: MidError,
}
#[derive(Error, Debug)]
#[error("Mid error")]
struct MidError {
#[source]
source: RootError,
}
#[derive(Error, Debug)]
#[error("Root error")]
struct RootError;
fn main() {
let err = TopError {
source: MidError { source: RootError },
};
println!("Root cause: {}", find_root_cause(&err));
println!("\nFull chain:\n{}", format_chain(&err));
}Utility functions can traverse the error chain for various purposes.
use std::error::Error;
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Wrapped error")]
struct WrappedError {
#[source]
source: Box<dyn Error + Send + Sync>,
}
#[derive(Error, Debug)]
#[error("Specific error: {code}")]
struct SpecificError {
code: i32,
}
fn find_specific_error(e: &dyn Error) -> Option<&SpecificError> {
let mut current = Some(e);
while let Some(err) = current {
if let Some(specific) = err.downcast_ref::<SpecificError>() {
return Some(specific);
}
current = err.source();
}
None
}
fn main() {
let specific = SpecificError { code: 404 };
let wrapped: WrappedError = WrappedError {
source: Box::new(specific),
};
if let Some(found) = find_specific_error(&wrapped) {
println!("Found specific error with code: {}", found.code);
}
}downcast_ref lets you find specific error types in a chain.
use std::error::Error;
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Operation failed")]
struct OperationError {
#[source]
source: std::io::Error,
}
fn main() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let op_err = OperationError { source: io_err };
// source() gives the causal chain
if let Some(s) = op_err.source() {
println!("Caused by: {}", s);
}
// std::error::Error also has provide() for backtraces (Rust 1.65+)
// but source() is specifically for error chains
}source() provides causal chain; backtraces are separate through provide().
use thiserror::Error;
use std::error::Error;
// Library error types should expose source for consumers
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Connection failed to {host}")]
Connection {
host: String,
#[source]
cause: std::io::Error,
},
#[error("Query failed: {query}")]
Query {
query: String,
#[source]
cause: QueryError,
},
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Syntax error")]
Syntax,
#[error("Timeout after {ms}ms")]
Timeout { ms: u64 },
}
// Application can walk the chain
fn handle_database_error(e: &DatabaseError) {
println!("Database error: {}", e);
let mut current = e.source();
while let Some(err) = current {
println!(" Caused by: {}", err);
current = err.source();
}
}Library errors should properly chain sources for debugging.
source() behavior by annotation:
| Annotation | Effect |
|------------|--------|
| #[source] | Field becomes source() return value |
| #[from] | Implements From AND sets as source |
| Field named source | Auto-detected as source |
| Other field names | Not a source without annotation |
Chain traversal pattern:
fn traverse_chain(e: &dyn Error) {
let mut current = Some(e as &dyn Error);
while let Some(err) = current {
println!("{}", err);
current = err.source();
}
}When to use each:
| Use Case | Approach |
|----------|----------|
| Wrapping errors with context | #[from] for conversion |
| Explicit source field | #[source] attribute |
| Dynamic error types | Box<dyn Error> with #[source] |
| Finding specific errors | Walk chain with downcast_ref |
| Root cause analysis | Traverse to end of chain |
Key insight: The source method in std::error::Error provides the fundamental mechanism for error chain traversal in Rust, but implementing it manually is tediousāyou must return Option<&dyn Error> from the correct field, handling lifetime and type concerns correctly. thiserror automates this by detecting source fields (named source or marked with #[source] or #[from]) and generating the implementation. This matters because error chains are essential for debugging: when an application error bubbles up through multiple layers, each wrapping the previous with additional context, the source() chain preserves the causal story. Without source(), you'd only see the outermost error messageālosing the critical information about what actually went wrong at the bottom of the stack. With proper source chains, you can walk from "Application failed to start" through "Configuration could not be loaded" to "File not found: /etc/myapp/config.toml"āthe actual actionable error. thiserror ensures every derived error type participates correctly in this chain, making error handling both ergonomic for developers and useful for operators.