What is the purpose of thiserror::Error::Error attribute for customizing source error chains?
The #[source] attribute in thiserror (applied within #[derive(Error)]) marks a field as the underlying cause of an error, enabling automatic error chain construction that integrates with Rust's std::error::Error trait's source() method. When you derive Error for an enum or struct, any field marked with #[source] or named source automatically provides the causal chainâcallers can traverse from your error to its underlying cause through Error::source(). This attribute lets you customize which field represents the error's origin, support multiple source fields for different variants, and control whether the source is included in the Display output. Understanding source chains is essential for building composable error types that preserve debugging context across abstraction boundaries.
The Error Source Mechanism
use thiserror::Error;
#[derive(Debug, Error)]
enum MyError {
#[error("Failed to read file")]
Io(#[source] std::io::Error),
#[error("Failed to parse JSON")]
Json(#[source] serde_json::Error),
}
fn example() {
let io_error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
);
let my_error = MyError::Io(io_error);
// The source chain:
// my_error -> Io(io_error) -> io_error.source()
use std::error::Error;
let source = my_error.source();
assert!(source.is_some());
println!("Source: {:?}", source);
}#[source] marks the field as the error's cause, accessible via Error::source().
Automatic Source Detection
use thiserror::Error;
#[derive(Debug, Error)]
enum DataError {
// Field named "source" is automatically detected
#[error("Database connection failed")]
Connection {
source: sqlx::Error, // Automatically used as source
},
// Explicit #[source] for non-standard names
#[error("Query execution failed")]
Query {
#[source]
inner: sqlx::Error, // Explicit source marker
},
// Multiple fields - must mark which is source
#[error("Transaction failed")]
Transaction {
#[source]
db_error: sqlx::Error,
context: String, // Not a source
},
}
// The Error::source() implementation returns the marked fieldFields named source are auto-detected; use #[source] for other names.
Source Without Display Inclusion
use thiserror::Error;
use std::io;
#[derive(Debug, Error)]
enum ConfigError {
// By default, #[source] doesn't include the source in Display
#[error("Failed to load config from {path}")]
Load {
#[source]
io_error: io::Error,
path: String,
},
// The source is available programmatically but not in the message
}
fn display_behavior() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
let err = ConfigError::Load {
io_error: io_err,
path: "/etc/config.toml".to_string(),
};
// Display shows only the error message
println!("{}", err); // "Failed to load config from /etc/config.toml"
// Source is accessible separately
use std::error::Error;
println!("Caused by: {}", err.source().unwrap());
// "file missing"
}#[source] separates the error message from its cause in the chain.
Source vs Source Attribute Variants
use thiserror::Error;
#[derive(Debug, Error)]
enum ParseError {
// Variant 1: #[source] attribute on field
#[error("Invalid input")]
Invalid {
#[source]
parse_error: std::num::ParseIntError,
},
// Variant 2: #[error(source)] shorthand
#[error("Conversion failed: {0}")]
Convert(#[source] std::num::TryFromIntError),
// Variant 3: Named "source" (implicit)
#[error("Processing failed")]
Process {
source: std::io::Error, // Auto-detected
},
}
// All three achieve the same: Error::source() returns the inner errorMultiple syntax options exist; choose based on readability and naming conventions.
Chaining Errors Across Layers
use thiserror::Error;
// Low-level error
#[derive(Debug, Error)]
enum DbError {
#[error("Connection refused")]
ConnectionRefused,
#[error("Query timeout")]
Timeout,
}
// Mid-level error wrapping DbError
#[derive(Debug, Error)]
enum RepositoryError {
#[error("Failed to connect to database")]
Connect {
#[source]
source: DbError,
},
#[error("Query execution failed")]
Query {
#[source]
source: DbError,
},
}
// High-level error wrapping RepositoryError
#[derive(Debug, Error)]
enum ServiceError {
#[error("Service unavailable")]
Unavailable {
#[source]
source: RepositoryError,
},
#[error("Operation failed")]
Failed {
#[source]
source: RepositoryError,
},
}
fn chain_example() {
use std::error::Error;
let db_err = DbError::ConnectionRefused;
let repo_err = RepositoryError::Connect { source: db_err };
let service_err = ServiceError::Unavailable { source: repo_err };
// Traverse the chain
let mut current: &dyn Error = &service_err;
loop {
println!("Error: {}", current);
match current.source() {
Some(source) => current = source,
None => break,
}
}
// Output:
// Error: Service unavailable
// Error: Failed to connect to database
// Error: Connection refused
}Source chains enable debugging across multiple abstraction layers.
Optional Sources
use thiserror::Error;
use std::io;
#[derive(Debug, Error)]
enum FileError {
#[error("Failed to process file")]
Process {
#[source]
io_error: io::Error, // Required source
},
#[error("File not found: {path}")]
NotFound {
path: String, // No source - this is a leaf error
},
}
// Some variants have sources, some don't
// Error::source() returns Option<&dyn Error>
fn optional_source() {
use std::error::Error;
let with_source = FileError::Process {
io_error: io::Error::new(io::ErrorKind::Other, "read error"),
};
assert!(with_source.source().is_some());
let without_source = FileError::NotFound {
path: "/missing/file".to_string(),
};
assert!(without_source.source().is_none());
}Not all error variants need sources; leaf errors have source() -> None.
Boxed Sources for Type Erasure
use thiserror::Error;
use std::io;
#[derive(Debug, Error)]
enum AppError {
// Box<dyn Error> allows any error type
#[error("Internal error")]
Internal {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
// Or Box specific error types
#[error("Subsystem error")]
Subsystem {
#[source]
source: Box<SubsystemError>,
},
}
#[derive(Debug, Error)]
#[error("Subsystem failure: {reason}")]
struct SubsystemError {
reason: String,
}
fn boxed_source() -> Result<(), AppError> {
// Can wrap any error type
let io_err = io::Error::new(io::ErrorKind::Other, "io failure");
Err(AppError::Internal {
source: Box::new(io_err),
})
}Box<dyn Error> enables flexible error types that accept any source.
Multiple Potential Sources
use thiserror::Error;
#[derive(Debug, Error)]
enum ProcessingError {
// Only ONE source per error
// If you have multiple errors, choose the primary cause
#[error("Input validation failed")]
Validation {
#[source]
input_error: ValidationError, // Primary source
context: String, // Additional info, not a source
},
#[error("Output failed")]
Output {
#[source]
output_error: std::io::Error,
},
}
#[derive(Debug, Error)]
#[error("Invalid field: {field}")]
struct ValidationError {
field: String,
}
// If you need multiple sources, consider:
// 1. Combining them into a single error type
// 2. Creating a wrapper that holds Vec<Box<dyn Error>>
// 3. Using anyhow/eyre for ad-hoc error chainsEach error variant can have at most one source field.
Source with Transparent Forwarding
use thiserror::Error;
#[derive(Debug, Error)]
enum WrapperError {
// #[error(transparent)] forwards both Display and source
#[error(transparent)]
Io(#[from] std::io::Error),
// Equivalent to:
// #[error("{0}")] // Uses inner's Display
// Io(#[source] std::io::Error),
}
// "transparent" means:
// - Display is forwarded from source
// - source() returns the inner error
// - Useful for simple wrappers
fn transparent_example() {
use std::error::Error;
let io_err = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
);
let wrapper = WrapperError::Io(io_err);
// Display is forwarded
println!("{}", wrapper); // "file not found"
// Source is the inner error
let source = wrapper.source().unwrap();
println!("{:?}", source);
}#[error(transparent)] forwards both display and source to the inner error.
Customizing Display vs Source
use thiserror::Error;
use std::num::ParseIntError;
#[derive(Debug, Error)]
enum NumberError {
// Display message is separate from source
#[error("Failed to parse number in field '{field}'")]
ParseField {
#[source]
source: ParseIntError, // The underlying error
field: String, // Context for display
},
// Source message included in display
#[error("Number parsing failed: {source}")]
ParseExplicit {
#[source]
source: ParseIntError,
},
// Using transparent for full forwarding
#[error(transparent)]
Transparent(ParseIntError),
}
fn display_vs_source() {
use std::error::Error;
let parse_err = "abc".parse::<i32>().unwrap_err();
// Separate display and source
let err1 = NumberError::ParseField {
source: parse_err.clone(),
field: "count".to_string(),
};
println!("{}", err1); // "Failed to parse number in field 'count'"
println!("Caused by: {}", err1.source().unwrap()); // "invalid digit found in string"
// Source in display
let err2 = NumberError::ParseExplicit {
source: parse_err,
};
println!("{}", err2); // "Number parsing failed: invalid digit found in string"
}Choose whether to include source in display based on desired error messages.
From Implementation with Source
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("IO operation failed")]
Io {
#[from]
source: std::io::Error,
},
#[error("Configuration error")]
Config {
#[from]
source: ConfigError,
},
}
#[derive(Debug, Error)]
#[error("Invalid configuration")]
struct ConfigError;
// #[from] automatically creates:
// impl From<std::io::Error> for AppError
// AND marks the field as #[source]
fn from_example() -> Result<(), AppError> {
// ? operator automatically converts
let file = std::fs::read_to_string("config.toml")?;
Ok(())
}
// #[from] is shorthand for:
// #[source] + impl From<Source> for AppError#[from] combines source marking with From implementation for ? operator.
Error Chain Formatting
use thiserror::Error;
use std::error::Error;
#[derive(Debug, Error)]
enum ChainError {
#[error("Layer 1 error")]
Layer1 {
#[source]
source: Layer2Error,
},
}
#[derive(Debug, Error)]
enum Layer2Error {
#[error("Layer 2 error")]
Layer2 {
#[source]
source: Layer3Error,
},
}
#[derive(Debug, Error)]
#[error("Layer 3 error (root)")]
struct Layer3Error;
fn print_chain(err: &dyn Error) {
println!("Error: {}", err);
let mut source = err.source();
let mut level = 1;
while let Some(s) = source {
println!(" Caused by: {}", s);
source = s.source();
level += 1;
}
}
fn chain_formatting() {
let err = ChainError::Layer1 {
source: Layer2Error::Layer2 {
source: Layer3Error,
},
};
print_chain(&err);
// Error: Layer 1 error
// Caused by: Layer 2 error
// Caused by: Layer 3 error (root)
}
// Or use anyhow/eyre for prettier chains:
// println!("{:?}", anyhow::Error::new(err));Source chains enable hierarchical error reporting.
Source for Anyhow Compatibility
use thiserror::Error;
#[derive(Debug, Error)]
enum MyError {
#[error("Failed to process")]
Process {
#[source]
source: anyhow::Error,
},
}
// thiserror errors work with anyhow's context()
// because they implement Error::source()
fn anyhow_compat() -> Result<(), anyhow::Error> {
// Can convert thiserror to anyhow
let my_err = MyError::Process {
source: anyhow::anyhow!("inner error"),
};
// anyhow can chain this
Err(anyhow::Error::new(my_err))
.context("while doing operation")
}#[source] enables compatibility with error handling libraries.
Real Example: Network Library
use thiserror::Error;
use std::io;
#[derive(Debug, Error)]
enum NetworkError {
#[error("Failed to resolve host '{host}'")]
Dns {
#[source]
source: dns::DnsError,
host: String,
},
#[error("Connection to {addr} failed")]
Connection {
#[source]
source: io::Error,
addr: String,
},
#[error("TLS handshake failed")]
Tls {
#[source]
source: tls::TlsError,
},
#[error("Request timeout after {ms}ms")]
Timeout { ms: u64 }, // No source - leaf error
#[error("Invalid response status: {status}")]
Status { status: u16 }, // No source - leaf error
}
mod dns {
use thiserror::Error;
#[derive(Debug, Error)]
#[error("DNS lookup failed")]
pub struct DnsError;
}
mod tls {
use thiserror::Error;
#[derive(Debug, Error)]
#[error("TLS error: {0}")]
pub struct TlsError(pub String);
}
// Clear chain: NetworkError -> io::Error / DnsError / TlsError
// Timeout and Status are leaf errors with no further causeReal-world errors mix variants with sources and leaf variants.
Practical Usage Patterns
use thiserror::Error;
use std::io;
// Pattern 1: Wrap with context
#[derive(Debug, Error)]
enum LoadError {
#[error("Failed to load config from '{path}'")]
Io {
#[source]
source: io::Error,
path: String, // Context included in message
},
}
// Pattern 2: Transparent forwarding
#[derive(Debug, Error)]
enum WrapperError {
#[error(transparent)]
Io(io::Error), // Just wrap, no additional message
}
// Pattern 3: Multiple error types
#[derive(Debug, Error)]
enum AppError {
#[error("Config error")]
Config {
#[source]
source: ConfigError,
},
#[error("Database error")]
Database {
#[source]
source: DbError,
},
}
// Pattern 4: Optional context
#[derive(Debug, Error)]
enum ProcessError {
#[error("Processing failed")]
Generic {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}Choose patterns based on how much context and wrapping you need.
Synthesis
Quick reference:
use thiserror::Error;
#[derive(Debug, Error)]
enum ExampleError {
// Named "source" - auto-detected
#[error("Failed to connect")]
Connect { source: std::io::Error },
// Explicit #[source] marker
#[error("Query failed: {query}")]
Query {
#[source]
db_error: sqlx::Error,
query: String,
},
// #[from] = #[source] + From impl
#[error("IO failed")]
Io(#[from] std::io::Error),
// Transparent - forward display and source
#[error(transparent)]
Other(#[from] anyhow::Error),
// Leaf error - no source
#[error("Not found: {0}")]
NotFound(String),
}
// Key behaviors:
// 1. source() returns the marked field
// 2. source is NOT included in Display by default
// 3. Only one source per variant
// 4. Named "source" is auto-detected
// 5. Use #[error(transparent)] for full forwardingKey insight: The #[source] attribute in thiserror establishes error causality chains by marking which field represents the underlying cause of an error. This enables Error::source() traversal, allowing callers to examine the full chain from high-level errors down to root causes. The attribute separates the error's display message from its causal chainâby default, sources don't appear in Display output but are accessible programmatically. Combined with #[from], it enables seamless error propagation with ? while preserving the full chain. For transparent wrapping, #[error(transparent)] forwards both display and source. Use source chains to build debuggable error types that preserve context across abstraction boundaries, ensuring production failures can be traced to their root cause.
