How does thiserror::Error derive #[source] attribute for explicit error source chaining?
The #[source] attribute in thiserror::Error explicitly marks a field as the underlying cause of an error, enabling error chaining by automatically implementing the std::error::Error::source() methodâthis allows errors to carry their causal chain, letting callers trace back through nested failures to understand the complete context of what went wrong. Unlike #[from] which also generates From implementations, #[source] focuses solely on establishing the error hierarchy.
The std::error::Error::source Method
use std::error::Error;
fn source_method_basics() {
// std::error::Error has a source() method for error chaining
// fn source(&self) -> Option<&(dyn Error + 'static)>
// This method returns the underlying cause of an error
// It enables error chains like:
// DatabaseError -> ConnectionError -> IoError
// thiserror's #[source] attribute implements this method for you
}The source() method is how Rust represents error causality.
Basic #[source] Usage
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[source] std::io::Error),
#[error("the data for key {0} is not available")]
MissingData(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
#[source]
source: ParseError,
},
}
#[derive(Error, Debug)]
#[error("parse error")]
pub struct ParseError(String);
fn basic_usage() -> Result<(), DataStoreError> {
// The #[source] field is automatically used for source()
// When Disconnect variant is created with an io::Error:
let io_err = std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"connection reset"
);
let err = DataStoreError::Disconnect(io_err);
// source() returns the underlying error
if let Some(source) = err.source() {
println!("Caused by: {}", source);
}
// Output: Caused by: connection reset
}The #[source] attribute tells thiserror which field implements source().
Error Chain Display
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("configuration error")]
Config {
#[source]
source: ConfigError,
},
#[error("database error")]
Database {
#[source]
source: DbError,
},
}
#[derive(Error, Debug)]
#[error("config file not found: {path}")]
pub struct ConfigError {
path: String,
#[source]
source: std::io::Error,
}
#[derive(Error, Debug)]
#[error("connection failed")]
pub struct DbError(#[source] std::io::Error);
fn display_error_chain() {
let io_err = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
);
let config_err = ConfigError {
path: "/etc/app.conf".to_string(),
source: io_err,
};
let app_err = AppError::Config { source: config_err };
// Print the full error chain
println!("Error: {}", app_err);
let mut source = app_err.source();
while let Some(err) = source {
println!("Caused by: {}", err);
source = err.source();
}
// Output:
// Error: configuration error
// Caused by: config file not found: /etc/app.conf
// Caused by: file not found
}Walking the source() chain reveals the complete error history.
#[source] vs #[from]
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error1 {
#[error("io error")]
Io(#[from] std::io::Error), // #[from] implies #[source]
}
#[derive(Error, Debug)]
pub enum Error2 {
#[error("io error")]
Io(#[source] std::io::Error), // Only #[source], no From impl
}
fn source_vs_from() -> Result<(), Error1> {
// #[from] allows automatic conversion:
let result: Result<String, std::io::Error> = Err(
std::io::Error::new(std::io::ErrorKind::NotFound, "not found")
);
// The ? operator works because #[from] implements From<io::Error>
let _ = result?; // Automatically converts io::Error to Error1::Io
Ok(())
}
fn source_only() -> Result<(), Error2> {
// #[source] does NOT implement From:
let result: Result<String, std::io::Error> = Err(
std::io::Error::new(std::io::ErrorKind::NotFound, "not found")
);
// This would NOT compile:
// let _ = result?; // Error: From<io::Error> not implemented for Error2
// Must manually wrap:
let _ = result.map_err(Error2::Io)?;
Ok(())
}#[from] = #[source] + From implementation; #[source] alone only sets the source.
When to Use #[source] Without #[from]
use thiserror::Error;
#[derive(Error, Debug)]
pub enum HttpError {
// Use #[source] when you need to transform the error message
// or add context before wrapping
#[error("request failed after {attempts} attempts: {source}")]
RetryExhausted {
attempts: u32,
#[source]
source: reqwest::Error,
},
// Use #[source] when the inner error is one of several variants
// and you don't want From to pick one variant
#[error("validation failed")]
Validation {
field: String,
#[source]
source: ValidationError,
},
// Use #[source] when you need to preserve all information
// from the source but want a custom Display
#[error("database transaction failed")]
Transaction {
transaction_id: u64,
#[source]
source: sqlx::Error,
},
}
#[derive(Error, Debug)]
#[error("field {field} is invalid: {reason}")]
pub struct ValidationError {
field: String,
reason: String,
}
// If we used #[from], we'd get:
// impl From<reqwest::Error> for HttpError
// which would lose the 'attempts' contextUse #[source] when you need to preserve or add context beyond automatic conversion.
Tuple Struct with #[source]
use thiserror::Error;
#[derive(Error, Debug)]
pub enum NetworkError {
#[error("connection timeout after {0}ms")]
Timeout(u64), // No source - just data
#[error("connection failed")]
Connection(#[source] std::io::Error), // Source is the only field
#[error("request to {0} failed")]
RequestFailed(String, #[source] std::io::Error), // Source is second field
}
// Tuple structs use positional #[source]
#[derive(Error, Debug)]
#[error("wrapped error")]
pub struct WrappedError(#[source] pub std::io::Error);Position matters for tuple structs; #[source] marks which field is the cause.
Struct with Multiple #[source] Candidates
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
// If multiple fields could be sources, only mark one with #[source]
#[error("write failed to {path}")]
WriteFailed {
path: String,
bytes_written: usize,
#[source]
source: std::io::Error, // This is the primary source
},
// Only ONE #[source] per variant
#[error("read failed from {path}")]
ReadFailed {
path: String,
#[source]
source: std::io::Error,
},
}Each variant can have at most one #[source] field.
Source with Optional Errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("configuration error in {file}")]
LoadError {
file: String,
#[source]
source: std::io::Error,
},
// Optional source - None means no underlying cause
#[error("validation error")]
ValidationError {
field: String,
#[source]
source: Option<ValidationError>,
},
// Box<dyn Error> for any error type
#[error("unexpected error")]
Unexpected(#[source] Box<dyn std::error::Error + Send + Sync>),
}
#[derive(Error, Debug)]
#[error("invalid value for {field}")]
pub struct ValidationError {
field: String,
}
// thiserror handles Option<Box<dyn Error>> etc. automatically#[source] works with Option, Box<dyn Error>, and other wrapper types.
Implementing std::error::Error Manually with source
use std::error::Error;
use std::fmt;
#[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())
}
}
// Compare with thiserror:
#[derive(Error, Debug)]
#[error("{message}")]
pub struct AutoError {
message: String,
#[source]
source: Option<Box<dyn Error + Send + Sync>>,
}
// Thiserror generates the same source() implementation automaticallythiserror generates the source() implementation from the #[source] attribute.
Source Chain Traversal Utility
use std::error::Error;
fn print_error_chain(err: &dyn Error) {
println!("Error: {}", err);
let mut source = err.source();
let mut depth = 1;
while let Some(cause) = source {
println!(" {} Caused by: {}", " ".repeat(depth), cause);
source = cause.source();
depth += 1;
}
}
fn collect_error_chain(err: &dyn Error) -> Vec<&dyn Error> {
let mut chain = vec![err];
let mut current = err.source();
while let Some(source) = current {
chain.push(source);
current = source.source();
}
chain
}
fn find_root_cause(err: &dyn Error) -> &dyn Error {
let mut current = err;
while let Some(source) = current.source() {
current = source;
}
current
}Walking the source chain is essential for debugging and logging.
Real-World Example: Database Error Hierarchy
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum DbError {
#[error("connection pool exhausted")]
PoolExhausted {
pool_size: usize,
wait_time_ms: u64,
},
#[error("connection failed")]
ConnectionFailed {
#[source]
source: std::io::Error,
endpoint: String,
},
#[error("query execution failed")]
QueryFailed {
query: String,
#[source]
source: sql::Error,
},
#[error("transaction rollback")]
Rollback {
transaction_id: u64,
#[source]
source: Box<dyn Error + Send + Sync>,
},
#[error("migration failed at version {version}")]
MigrationFailed {
version: u64,
#[source]
source: MigrationError,
},
}
#[derive(Error, Debug)]
pub enum MigrationError {
#[error("SQL syntax error in migration {id}")]
Syntax {
id: u64,
#[source]
source: sql::Error,
},
#[error("constraint violation")]
Constraint {
table: String,
constraint: String,
#[source]
source: sql::Error,
},
}
mod sql {
use thiserror::Error;
#[derive(Error, Debug)]
#[error("SQL error: {message}")]
pub struct Error {
pub message: String,
#[source]
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
}
fn example_usage() {
let sql_err = sql::Error {
message: "syntax error".to_string(),
source: None,
};
let migration_err = MigrationError::Syntax {
id: 5,
source: sql_err,
};
let db_err = DbError::MigrationFailed {
version: 5,
source: migration_err,
};
print_error_chain(&db_err);
// Output:
// Error: migration failed at version 5
// Caused by: SQL syntax error in migration 5
// Caused by: SQL error: syntax error
}
fn print_error_chain(err: &dyn Error) {
println!("Error: {}", err);
let mut source = err.source();
while let Some(cause) = source {
println!("Caused by: {}", cause);
source = cause.source();
}
}Layered errors with #[source] create meaningful error hierarchies.
Real-World Example: HTTP Client Error
use thiserror::Error;
use std::error::Error;
#[derive(Error, Debug)]
pub enum HttpClientError {
#[error("DNS resolution failed for {host}")]
Dns {
host: String,
#[source]
source: dns::Error,
},
#[error("TLS handshake failed")]
Tls {
#[source]
source: tls::Error,
},
#[error("HTTP request failed")]
Http {
#[source]
source: http::Error,
},
#[error("response body too large: {size} bytes (max: {max})")]
BodyTooLarge {
size: usize,
max: usize,
// No source - this is a leaf error
},
#[error("request timed out after {timeout_ms}ms")]
Timeout {
timeout_ms: u64,
// No source - timeout is a leaf error
},
}
mod dns {
use thiserror::Error;
#[derive(Error, Debug)]
#[error("DNS error: {message}")]
pub struct Error {
pub message: String,
#[source]
pub source: Option<std::io::Error>,
}
}
mod tls {
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("certificate verification failed")]
Certificate {
#[source]
source: std::io::Error,
},
#[error("handshake failed")]
Handshake {
#[source]
source: std::io::Error,
},
}
}
mod http {
use thiserror::Error;
#[derive(Error, Debug)]
#[error("HTTP error: {0}")]
pub struct Error(pub String);
}
fn analyze_http_error() {
let io_err = std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"connection refused"
);
let dns_err = dns::Error {
message: "no records found".to_string(),
source: Some(io_err),
};
let http_err = HttpClientError::Dns {
host: "example.com".to_string(),
source: dns_err,
};
// Walk the chain
println!("Full error chain:");
print_error_chain(&http_err);
// Find root cause
let root = find_root_cause(&http_err);
println!("\nRoot cause: {}", root);
}Complex error hierarchies benefit from explicit #[source] annotation.
thiserror Generated Code
// What thiserror generates for:
#[derive(Error, Debug)]
pub enum MyError {
#[error("io error")]
Io(#[source] std::io::Error),
#[error("config error")]
Config {
path: String,
#[source]
source: ConfigError,
},
}
// Generated source() implementation (simplified):
impl std::error::Error for MyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
MyError::Io(source) => Some(source),
MyError::Config { source, .. } => Some(source),
}
}
}
// Without #[source], source() returns None:
#[derive(Error, Debug)]
pub enum LeafError {
#[error("not found")]
NotFound,
#[error("invalid input")]
InvalidInput,
}
impl std::error::Error for LeafError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None // No source fields
}
}The #[source] attribute determines what source() returns for each variant.
Source and Error Compatibility
use thiserror::Error;
// #[source] requires the type to implement std::error::Error
#[derive(Error, Debug)]
pub enum GoodError {
#[error("io error")]
Io(#[source] std::io::Error), // io::Error implements Error - OK
}
// This would NOT compile:
// #[derive(Error, Debug)]
// pub enum BadError {
// #[error("string error")]
// String(#[source] String), // String does NOT implement Error
// }
// For non-Error types, use #[from] or handle differently:
#[derive(Error, Debug)]
pub enum StringError {
#[error("string error: {0}")]
String(String), // No #[source] - String doesn't implement Error
}
// Or wrap in a type that implements Error:
#[derive(Error, Debug)]
#[error("{0}")]
pub struct StringWrapper(String);
#[derive(Error, Debug)]
pub enum WrappedError {
#[error("wrapped")]
Wrapped(#[source] StringWrapper),
}The #[source] field type must implement std::error::Error.
Multiple Error Sources
use thiserror::Error;
// Only ONE #[source] per variant, but can have multiple causes
#[derive(Error, Debug)]
pub enum TransactionError {
// Primary source is the main cause
#[error("transaction failed")]
Failed {
transaction_id: u64,
#[source]
source: DbError, // Primary cause
// Other errors can be stored but not marked as #[source]
network_error: Option<std::io::Error>,
},
}
// For multiple causes, create a compound source:
#[derive(Error, Debug)]
pub enum CompoundError {
#[error("multiple failures")]
Multiple {
#[source]
source: AggregateError,
},
}
#[derive(Error, Debug)]
#[error("aggregate error")]
pub struct AggregateError {
errors: Vec<Box<dyn std::error::Error + Send + Sync>>,
}
impl std::error::Error for AggregateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// Return first error as source
self.errors.first().map(|e| e.as_ref())
}
}Only one field can be #[source], but you can create compound error types.
Summary Table
use thiserror::Error;
// | Attribute | Implements source() | Implements From | Use Case |
// |-----------|---------------------|------------------|----------|
// | #[source] | Yes | No | Preserve context |
// | #[from] | Yes (implicitly) | Yes | Simple wrapping |
// | (neither) | No | No | Leaf errors |
// #[source] - error is part of error chain, but no automatic conversion
#[derive(Error, Debug)]
pub enum SourceError {
#[error("failed")]
Failed {
context: String,
#[source]
source: std::io::Error,
},
}
// #[from] - error is part of chain AND enables ? operator
#[derive(Error, Debug)]
pub enum FromError {
#[error("io error")]
Io(#[from] std::io::Error),
}
// No attribute - leaf error with no cause
#[derive(Error, Debug)]
pub enum LeafError {
#[error("not found")]
NotFound,
}Synthesis
use thiserror::Error;
use std::error::Error;
// Use #[source] when:
// 1. You need to preserve error context
// 2. You want to control the Display message separately
// 3. You don't want automatic From conversion
// 4. You need to transform errors before wrapping
#[derive(Error, Debug)]
pub enum AppError {
#[error("failed to load config from {path}")]
ConfigLoad {
path: String,
#[source]
source: std::io::Error,
// Context preserved, custom message, no From impl
},
#[error("database error")]
Database(#[from] sql::Error), // Use #[from] for simple wrapping
#[error("validation failed")]
Validation, // Leaf error - no source
}
// The key insight: #[source] establishes error causality
// without automatic conversion. This gives you:
// - Full control over the Display message
// - Ability to add context fields
// - Explicit error construction
// - Clean error chains for debuggingKey insight: The #[source] attribute is thiserror's mechanism for implementing std::error::Error::source(), which returns the underlying cause of an error. This enables error chainsâeach error can point to its cause, creating a linked list from a high-level "transaction failed" error down through "database connection lost" to "connection refused." The difference between #[source] and #[from] is subtle but important: #[from] includes everything #[source] does plus generates From<SourceType> for YourError, enabling the ? operator for automatic conversion. Use #[from] when you want simple "wrap and propagate" semanticsâIoError becomes MyError::Io(IoError) automatically. Use #[source] when you need to add contextâlike including the file path that failed, the number of retry attempts, or any other information that transforms the raw error into something meaningful at your abstraction level. The source chain is invaluable for debugging: walking error.source() recursively reveals the complete history of what went wrong, from user-facing message down to root cause. Without #[source], errors are "leaf" nodes with no history; with #[source], every error can tell its story.
