How does thiserror::Error::source implement error chain inspection for debugging?
thiserror::Error::source automatically implements the std::error::Error::source method for error types by leveraging the #[source] attribute, enabling error chain traversal for debugging and logging. When an error wraps another error, the source method returns a reference to the underlying error, allowing tools to walk the entire error chain. This chain inspection reveals the complete causal sequence from a high-level error down to the root cause, which is essential for understanding complex failures in production systems. The thiserror derive macro generates the source() implementation automatically, extracting source errors from fields marked with #[source] or fields named source.
Basic Error with Source
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Failed to process file")]
struct FileError {
#[source]
inner: std::io::Error,
}
fn main() {
let err = FileError {
inner: std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
};
// source() returns the underlying error
if let Some(source) = err.source() {
println!("Caused by: {}", source);
// Caused by: file not found
}
}The #[source] attribute tells thiserror to implement source() returning this field.
Automatic Source Detection
use thiserror::Error;
// Field named "source" is automatically used
#[derive(Error, Debug)]
#[error("Processing failed")]
struct ProcessingError {
source: std::io::Error, // No #[source] needed
}
// Field named "source" in enum variants
#[derive(Error, Debug)]
enum AppError {
#[error("IO error occurred")]
Io {
source: std::io::Error, // Automatically used as source
},
#[error("Parsing failed")]
Parse {
#[source]
inner: serde_json::Error, // Explicit #[source]
},
}
fn main() {
let err = AppError::Io {
source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied"),
};
// source() works automatically
println!("Caused by: {}", err.source().unwrap());
}Fields named source are automatically detected; other names require #[source].
Walking Error Chains
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
#[error("Configuration error")]
struct ConfigError {
#[source]
inner: ParseError,
}
#[derive(Error, Debug)]
#[error("Parse error at line {line}")]
struct ParseError {
line: usize,
#[source]
inner: LexerError,
}
#[derive(Error, Debug)]
#[error("Unexpected character '{char}'")]
struct LexerError {
char: char,
}
fn main() {
let err = ConfigError {
inner: ParseError {
line: 42,
inner: LexerError { char: '!' },
},
};
// Walk the entire error chain
let mut current: &dyn Error = &err;
println!("Error: {}", current);
while let Some(source) = current.source() {
println!("Caused by: {}", source);
current = source;
}
// Output:
// Error: Configuration error
// Caused by: Parse error at line 42
// Caused by: Unexpected character '!'
}Walking source() recursively reveals the complete error chain.
Standard Error Trait Integration
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
#[error("Database error")]
struct DatabaseError {
#[source]
inner: ConnectionError,
}
#[derive(Error, Debug)]
#[error("Connection failed to {host}")]
struct ConnectionError {
host: String,
#[source]
inner: std::io::Error,
}
fn main() {
let err = DatabaseError {
inner: ConnectionError {
host: "localhost".to_string(),
inner: std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"connection refused"
),
},
};
// Use std::error::Error methods
let err: &dyn Error = &err;
// Display shows the error message
println!("Error: {}", err);
// source() shows the immediate cause
if let Some(source) = err.source() {
println!("Source: {}", source);
}
// Chain::new() (in some crates) or manual iteration walks the chain
let mut chain = vec![err.to_string()];
let mut current = err.source();
while let Some(source) = current {
chain.push(source.to_string());
current = source.source();
}
println!("Full chain: {:?}", chain);
}thiserror integrates seamlessly with std::error::Error for tooling compatibility.
Error Chain with anyhow
use thiserror::Error;
use anyhow::{Context, Result};
#[derive(Error, Debug)]
#[error("User lookup failed")]
struct UserLookupError {
#[source]
inner: DatabaseError,
}
#[derive(Error, Debug)]
#[error("Database error")]
struct DatabaseError {
#[source]
inner: std::io::Error,
}
fn main() -> Result<()> {
// anyhow preserves error chains with context
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "user not found");
let db_err = DatabaseError { inner: io_err };
let user_err = UserLookupError { inner: db_err };
// anyhow can wrap thiserror types
let err: anyhow::Error = user_err.into();
// Print full chain
for cause in err.chain() {
println!("Caused by: {}", cause);
}
Ok(())
}thiserror errors work with anyhow for enhanced error handling.
Multiple Source Fields
use thiserror::Error;
#[derive(Error, Debug)]
enum MultiError {
#[error("Failed to load config")]
Config {
#[source]
parse_error: serde_json::Error,
},
#[error("Failed to connect")]
Connection {
#[source]
io_error: std::io::Error,
},
#[error("Failed to validate")]
Validation {
#[source]
validation_error: ValidationError,
},
}
#[derive(Error, Debug)]
#[error("Validation failed: {message}")]
struct ValidationError {
message: String,
}
fn main() {
let err = MultiError::Config {
parse_error: serde_json::from_str::<serde_json::Value>("invalid").unwrap_err(),
};
if let Some(source) = err.source() {
println!("Parse error: {}", source);
}
}Each variant can have its own source error type.
Optional Source Errors
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Operation failed")]
struct OperationError {
#[source]
inner: Option<std::io::Error>,
}
// thiserror handles Option<Error> correctly
// source() returns Some only when the Option is Some
fn main() {
// With source
let err_with_source = OperationError {
inner: Some(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"timeout"
)),
};
println!("Has source: {}", err_with_source.source().is_some());
// Without source
let err_without_source = OperationError { inner: None };
println!("Has source: {}", err_without_source.source().is_none());
}Option<Error> fields work correctly with source().
Box Sources
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
#[error("Application error")]
struct AppError {
#[source]
inner: Box<dyn Error + Send + Sync>,
}
fn main() {
let err = AppError {
inner: Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"resource not found"
)),
};
// source() returns the boxed error
if let Some(source) = err.source() {
println!("Caused by: {}", source);
}
}Boxed errors work as sources, enabling type-erased error chains.
Source Attribute Variations
use thiserror::Error;
// #[source] attribute
#[derive(Error, Debug)]
#[error("IO error")]
struct IoError {
#[source]
underlying: std::io::Error,
}
// #[source] with explicit type
#[derive(Error, Debug)]
#[error("Network error")]
struct NetworkError {
#[source]
io: std::io::Error,
}
// Automatic detection by name
#[derive(Error, Debug)]
#[error("Parse error")]
struct ParseError {
source: serde_json::Error, // Automatically detected
}
// Transparent error forwarding
#[derive(Error, Debug)]
#[error(transparent)] // Forwards both Display and source
struct TransparentError(#[source] std::io::Error);
fn main() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let err1 = IoError { underlying: io_err };
let err2 = NetworkError { io: std::io::Error::new(std::io::ErrorKind::Other, "other") };
let err3 = ParseError { source: serde_json::from_str("invalid").unwrap_err() };
let err4 = TransparentError(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken"));
// All implement source() correctly
println!("Source: {:?}", err1.source());
println!("Source: {:?}", err2.source());
println!("Source: {:?}", err3.source());
println!("Source: {:?}", err4.source());
}Multiple syntax options for declaring source errors.
Transparent Errors
use thiserror::Error;
// #[error(transparent)] forwards Display and source
#[derive(Error, Debug)]
#[error(transparent)]
struct WrappedError(#[source] std::io::Error);
// Useful for wrapping external errors
#[derive(Error, Debug)]
enum AppError {
#[error(transparent)]
Io(#[source] std::io::Error),
#[error(transparent)]
Json(#[source] serde_json::Error),
#[error("Custom error: {0}")]
Custom(String),
}
fn main() {
let err = AppError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file missing"
));
// Display is forwarded from the source
println!("Error: {}", err); // "file missing"
// source() is also forwarded
if let Some(source) = err.source() {
println!("Source: {}", source);
}
}#[error(transparent)] creates a passthrough for both Display and source.
Custom Source Implementation
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
#[error("Multi-cause error")]
struct MultiCauseError {
primary: std::io::Error,
secondary: Option<std::io::Error>,
}
// Implement source() manually when needed
impl Error for MultiCauseError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.primary)
}
}
// Or let thiserror handle it:
#[derive(Error, Debug)]
#[error("Single cause error")]
struct SingleCauseError {
#[source]
cause: std::io::Error,
}
fn main() {
let err = SingleCauseError {
cause: std::io::Error::new(std::io::ErrorKind::Other, "test"),
};
// thiserror-generated source()
println!("Source: {}", err.source().unwrap());
}You can implement source() manually, but thiserror handles common cases.
Debugging with Error Chains
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
#[error("HTTP request failed")]
struct HttpError {
url: String,
#[source]
inner: NetworkError,
}
#[derive(Error, Debug)]
#[error("Network error")]
struct NetworkError {
#[source]
inner: std::io::Error,
}
fn print_error_chain(err: &dyn Error) {
println!("Error: {}", err);
let mut level = 1;
let mut current = err.source();
while let Some(source) = current {
println!(" {}: {}", level, source);
level += 1;
current = source.source();
}
}
fn main() {
let err = HttpError {
url: "https://example.com".to_string(),
inner: NetworkError {
inner: std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"connection reset by peer"
),
},
};
print_error_chain(&err);
// Error: HTTP request failed
// 1: Network error
// 2: connection reset by peer
}Walking the source chain provides complete error context for debugging.
Integration with Logging
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
#[error("Service unavailable: {service}")]
struct ServiceError {
service: String,
#[source]
inner: ConnectionError,
}
#[derive(Error, Debug)]
#[error("Connection timeout")]
struct ConnectionError;
fn log_error_chain(err: &dyn Error) {
// Log the top-level error
eprintln!("ERROR: {}", err);
// Log each cause
let mut current = err.source();
while let Some(cause) = current {
eprintln!(" caused by: {}", cause);
current = cause.source();
}
}
fn main() {
let err = ServiceError {
service: "database".to_string(),
inner: ConnectionError,
};
log_error_chain(&err);
}Error chains integrate naturally with logging for debugging production issues.
Source and Backtraces
use thiserror::Error;
use std::backtrace::Backtrace;
#[derive(Error, Debug)]
#[error("Operation failed")]
struct OperationError {
#[source]
inner: std::io::Error,
backtrace: Backtrace,
}
fn main() {
let err = OperationError {
inner: std::io::Error::new(std::io::ErrorKind::Other, "failed"),
backtrace: Backtrace::capture(),
};
// source() returns the error
if let Some(source) = err.source() {
println!("Source: {}", source);
}
// Backtrace is separate (RUST_BACKTRACE=1 to enable)
// println!("Backtrace: {}", err.backtrace);
}Source chains and backtraces provide complementary debugging information.
Error Source in Tests
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
#[error("Validation error")]
struct ValidationError {
field: String,
#[source]
inner: ParseError,
}
#[derive(Error, Debug)]
#[error("Invalid format")]
struct ParseError;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_source() {
let err = ValidationError {
field: "email".to_string(),
inner: ParseError,
};
// Verify source chain
let source = err.source().expect("should have source");
assert_eq!(source.to_string(), "Invalid format");
}
#[test]
fn test_error_chain_length() {
let err = ValidationError {
field: "email".to_string(),
inner: ParseError,
};
let mut count = 0;
let mut current = err.source();
while let Some(source) = current {
count += 1;
current = source.source();
}
assert_eq!(count, 1);
}
}Test error chains to verify proper error wrapping.
Generic Error Types
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Wrapper error")]
struct WrapperError<E: std::error::Error + 'static> {
#[source]
inner: E,
}
#[derive(Error, Debug)]
#[error("Inner error")]
struct InnerError;
fn main() {
let err = WrapperError { inner: InnerError };
println!("Error: {}", err);
if let Some(source) = err.source() {
println!("Source: {}", source);
}
}Generic error types can use #[source] with type parameters.
Source with Anyhow Context
use thiserror::Error;
use anyhow::{Context, Result};
#[derive(Error, Debug)]
#[error("Processing failed")]
struct ProcessingError {
#[source]
inner: std::io::Error,
}
fn process_file() -> Result<()> {
std::fs::read_to_string("config.json")
.map_err(|e| ProcessingError { inner: e })?;
Ok(())
}
fn main() -> Result<()> {
process_file()
.context("Failed during startup")?;
Ok(())
}
// The error chain now includes:
// 1. "Failed during startup" (from anyhow context)
// 2. "Processing failed" (from thiserror)
// 3. The actual io::Error messagethiserror errors integrate with anyhow context for richer chains.
Error Display vs Source
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Failed to load user {user_id}")]
struct UserLoadError {
user_id: u64,
#[source]
db_error: DatabaseError,
}
#[derive(Error, Debug)]
#[error("Database query failed")]
struct DatabaseError {
query: String,
#[source]
io_error: std::io::Error,
}
fn main() {
let err = UserLoadError {
user_id: 42,
db_error: DatabaseError {
query: "SELECT * FROM users".to_string(),
io_error: std::io::Error::new(
std::io::ErrorKind::TimedOut,
"query timeout"
),
},
};
// Display shows the top-level message
println!("Display: {}", err);
// "Failed to load user 42"
// source() shows the underlying cause
println!("Source: {}", err.source().unwrap());
// "Database query failed"
// Chain walks deeper
println!("Deep source: {}", err.source().unwrap().source().unwrap());
// "query timeout"
}Display shows the immediate error; source() enables traversing to root causes.
API Summary
use thiserror::Error;
use std::error::Error;
// Basic source
#[derive(Error, Debug)]
#[error("Error occurred")]
struct BasicError {
#[source]
inner: std::io::Error,
}
// Automatic source by name
#[derive(Error, Debug)]
#[error("Error occurred")]
struct AutoSourceError {
source: std::io::Error, // Automatically detected
}
// Optional source
#[derive(Error, Debug)]
#[error("Error occurred")]
struct OptionalSourceError {
#[source]
inner: Option<std::io::Error>,
}
// Transparent forwarding
#[derive(Error, Debug)]
#[error(transparent)]
struct TransparentError(#[source] std::io::Error);
// Use in code
fn main() {
let err = BasicError {
inner: std::io::Error::new(std::io::ErrorKind::Other, "test"),
};
// Access via Error trait
let err: &dyn Error = &err;
// Walk the chain
if let Some(source) = err.source() {
println!("Caused by: {}", source);
}
}Synthesis
How thiserror implements source:
| Attribute | Effect |
|---|---|
#[source] on field |
Implements source() to return that field |
Field named source |
Automatically treated as source |
#[error(transparent)] |
Forwards both Display and source |
| No source attribute | source() returns None |
Error chain inspection pattern:
fn print_chain(err: &dyn Error) {
println!("Error: {}", err);
let mut current = err.source();
while let Some(cause) = current {
println!("Caused by: {}", cause);
current = cause.source();
}
}Benefits of source chains:
| Benefit | Explanation |
|---|---|
| Root cause analysis | Walk from surface error to root cause |
| Context preservation | Each layer adds meaningful context |
| Debugging | Tools can display full error history |
| Logging | Chain provides complete failure picture |
Key insight: thiserror::Error automates implementation of std::error::Error::source() by inspecting fields marked with #[source] or named source, generating code that returns a reference to the underlying error. This enables standard error chain traversal: starting from any error, repeatedly calling source() walks down the causal chain to the root cause. The chain provides context for debugging: each error in the chain can represent a different abstraction layer (network â protocol â application â user-facing), with source() connecting them. The #[error(transparent)] attribute creates a passthrough error that forwards both Display (error message) and source, useful for wrapping external errors without adding new context. Error chains integrate with the ecosystem: anyhow preserves chains when adding context, logging frameworks can walk chains to provide complete error output, and test assertions can verify proper error wrapping. The pattern of wrapping errors with additional context at each layerâwhile preserving the source chainâcreates self-documenting failure paths that explain not just what failed, but the complete sequence of events leading to the failure.
