How does thiserror::Error::from derive implementation differ from impl From<Inner> for MyError?
The #[from] attribute in thiserror generates both the From<SourceError> implementation and the source() method for the Error trait, while manually implementing From only provides the conversion function. This means #[from] gives you proper error chaining for freeβthe error source is automatically tracked and can be accessed via error::Error::source(). A manual From implementation leaves error source tracking as your responsibility, which is easy to forget and results in incomplete error reporting.
The From Trait: Manual Implementation
use std::error::Error;
#[derive(Debug)]
enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
// Manual From implementation - just does conversion
impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> Self {
MyError::Io(err)
}
}
impl From<std::num::ParseIntError> for MyError {
fn from(err: std::num::ParseIntError) -> Self {
MyError::Parse(err)
}
}
fn main() {
// Now you can use ? operator
fn read_config() -> Result<String, MyError> {
let content = std::fs::read_to_string("config.txt")?; // Works!
Ok(content)
}
// But check the Error implementation:
let err = MyError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"));
// Error trait is NOT derived - source() returns None
// (unless you implement Error manually)
// This is what's missing from manual From:
// fn source(&self) -> Option<&(dyn Error + 'static)> {
// match self {
// MyError::Io(e) => Some(e),
// MyError::Parse(e) => Some(e),
// }
// }
}Manual From only provides conversion; error sources aren't automatically tracked.
Thiserror's #[from] Attribute
use thiserror::Error;
#[derive(Debug, Error)]
enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
}
fn main() {
// #[from] generates BOTH:
// 1. impl From<std::io::Error> for MyError
// 2. impl Error for MyError { fn source() -> ... }
let err: MyError = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found").into();
// Error source is now properly tracked!
assert!(err.source().is_some());
// The chain is preserved for error reporting
println!("Error: {}", err);
if let Some(source) = err.source() {
println!("Caused by: {}", source);
}
}#[from] generates conversion AND proper error source tracking.
What #[from] Actually Generates
use thiserror::Error;
use std::error::Error;
// Given this:
#[derive(Debug, Error)]
enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
}
// thiserror generates approximately:
// 1. From implementation
impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
AppError::Database(err)
}
}
impl From<reqwest::Error> for AppError {
fn from(err: reqwest::Error) -> Self {
AppError::Http(err)
}
}
// 2. Display implementation (from #[error(...)])
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Database(e) => write!(f, "Database error: {}", e),
AppError::Http(e) => write!(f, "HTTP error: {}", e),
}
}
}
// 3. Error trait with source()
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Database(e) => Some(e),
AppError::Http(e) => Some(e),
}
}
}
fn main() {
// Now error chains work properly
let db_error = sqlx::Error::RowNotFound;
let app_error: AppError = db_error.into();
// Source chain is preserved
println!("{}", app_error); // "Database error: ..."
println!("Source: {:?}", app_error.source()); // Some(sqlx::Error::RowNotFound)
}#[from] generates three pieces: From, Display, and Error::source.
Manual From: What's Missing
use std::error::Error;
use std::fmt;
#[derive(Debug)]
enum AppError {
Database(sqlx::Error),
Http(reqwest::Error),
}
// Manual From implementations
impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
AppError::Database(err)
}
}
impl From<reqwest::Error> for AppError {
fn from(err: reqwest::Error) -> Self {
AppError::Http(err)
}
}
// Manual Display
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Database(e) => write!(f, "Database error: {}", e),
AppError::Http(e) => write!(f, "HTTP error: {}", e),
}
}
}
// Now we need Error implementation for proper source tracking
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::Database(e) => Some(e),
AppError::Http(e) => Some(e),
}
}
}
fn main() {
// This works now, but we had to write all of it manually!
// And it's easy to forget the Error implementation
// Common mistake: implementing From but forgetting Error::source
// Result: error chains break in logs/reports
}Manual implementation requires From, Display, and Error::sourceβeasy to miss one.
Struct Errors with #[from]
use thiserror::Error;
// #[from] works on struct fields too
#[derive(Debug, Error)]
#[error("Failed to process request")]
struct ProcessError {
#[from]
source: std::io::Error,
}
// This is equivalent to:
#[derive(Debug, Error)]
#[error("Failed to process request")]
struct ProcessErrorManual {
source: std::io::Error,
}
impl From<std::io::Error> for ProcessErrorManual {
fn from(source: std::io::Error) -> Self {
Self { source }
}
}
impl std::error::Error for ProcessErrorManual {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
fn main() {
// Both work the same way
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let process_err: ProcessError = io_err.into();
println!("Error: {}", process_err);
println!("Source: {:?}", process_err.source());
}#[from] on struct fields generates the same functionality.
Multiple Error Sources
use thiserror::Error;
// Only ONE field can have #[from] per variant
#[derive(Debug, Error)]
enum MultiError {
#[error("Database error")]
Database {
#[from]
error: sqlx::Error,
},
#[error("HTTP error with context")]
Http {
#[from]
error: reqwest::Error,
url: String, // Additional context
},
// This would be an error:
// #[error("Multiple from")]
// Multiple {
// #[from] db: sqlx::Error,
// #[from] http: reqwest::Error, // ERROR: multiple #[from]
// },
}
// If you need multiple sources, handle it manually:
#[derive(Debug, Error)]
enum MultiSourceError {
#[error("Multiple errors occurred")]
Multiple {
db: Option<sqlx::Error>,
http: Option<reqwest::Error>,
},
}
// Then implement From manually for each path
fn main() {
let http_err = reqwest::Error::new(
reqwest::StatusCode::NOT_FOUND,
"not found".into()
);
let err: MultiError = http_err.into();
}Only one field per variant can have #[from].
#[from] with #[source]
use thiserror::Error;
// #[from] implies #[source]
// These are equivalent:
#[derive(Debug, Error)]
enum ErrorA {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Error)]
enum ErrorB {
#[error("IO error: {0}")]
Io(#[source] std::io::Error),
// ^ #[source] marks field for Error::source()
// but doesn't generate From
}
// Use #[source] when you want source tracking without From:
// - Source isn't a direct input (has other required fields)
// - Conversion needs custom logic
#[derive(Debug, Error)]
#[error("Config error in {file}: {source}")]
struct ConfigError {
file: String,
#[source]
source: std::io::Error,
}
// Manual From needed for ConfigError:
impl From<std::io::Error> for ConfigError {
fn from(source: std::io::Error) -> Self {
Self {
file: String::new(), // We don't have file!
source,
}
}
}
// Better: don't derive From, require explicit construction:
fn load_config(file: &str) -> Result<String, ConfigError> {
std::fs::read_to_string(file).map_err(|e| ConfigError {
file: file.to_string(),
source: e,
})
}
fn main() {
// ConfigError has source tracking without automatic From
}#[from] is #[source] plus automatic From generation.
Error Chaining in Practice
use thiserror::Error;
use std::error::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("Failed to read config: {0}")]
Config(#[from] std::io::Error),
#[error("Failed to parse value: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("Failed to connect: {0}")]
Connection(#[from] std::io::Error),
}
fn read_value() -> Result<i32, AppError> {
let content = std::fs::read_to_string("config.txt")?; // Io -> AppError::Config
let value: i32 = content.trim().parse()?; // ParseInt -> AppError::Parse
Ok(value)
}
fn main() {
match read_value() {
Ok(v) => println!("Value: {}", v),
Err(e) => {
// Full error chain available
println!("Error: {}", e);
// Walk the chain
let mut source = e.source();
while let Some(cause) = source {
println!("Caused by: {}", cause);
source = cause.source();
}
}
}
// Output might be:
// Error: Failed to read config: No such file or directory (os error 2)
// Caused by: No such file or directory (os error 2)
}#[from] ensures error chains are preserved through source().
When Manual From Makes Sense
use std::error::Error;
use std::fmt;
// Manual From when you need custom conversion logic
#[derive(Debug)]
enum ConversionError {
Int(std::num::ParseIntError),
Float(std::num::ParseFloatError),
Custom(String),
}
// Custom conversion: wrap with context
impl From<std::num::ParseIntError> for ConversionError {
fn from(err: std::num::ParseIntError) -> Self {
// Could add context, logging, etc.
ConversionError::Int(err)
}
}
impl From<std::num::ParseFloatError> for ConversionError {
fn from(err: std::num::ParseFloatError) -> Self {
ConversionError::Float(err)
}
}
// thiserror can't do this: custom From implementations
impl From<String> for ConversionError {
fn from(msg: String) -> Self {
ConversionError::Custom(msg)
}
}
// When to use manual:
// 1. Multiple source types convert to one variant
// 2. Conversion needs custom logic
// 3. You need to add context during conversion
// 4. Source isn't stored directly (transformed)
#[derive(Debug, thiserror::Error)]
enum SmartError {
#[error("Invalid value: {value} (expected {expected})")]
InvalidValue { value: String, expected: String },
// Can't use #[from] here - conversion would lose context
}
impl From<std::num::ParseIntError> for SmartError {
fn from(e: std::num::ParseIntError) -> Self {
// Custom logic: extract context from error
SmartError::InvalidValue {
value: "unknown".to_string(),
expected: "integer".to_string(),
}
}
}
fn main() {}Use manual From when conversion needs custom logic or context.
Synthesis
Quick reference:
use thiserror::Error;
// #[from] provides:
// 1. impl From<Source> for MyError
// 2. impl Error for MyError { fn source() -> ... }
// 3. Display (from #[error(...)])
#[derive(Debug, Error)]
enum AutoError {
#[error("IO failed: {0}")]
Io(#[from] std::io::Error),
// Generates:
// - From<std::io::Error>
// - Error::source() returning the io::Error
}
// Manual From provides:
// 1. impl From<Source> for MyError
// (Just the conversion, no Error::source)
#[derive(Debug)]
enum ManualError {
Io(std::io::Error),
}
impl From<std::io::Error> for ManualError {
fn from(e: std::io::Error) -> Self {
ManualError::Io(e)
}
}
// Error::source() NOT implemented!
// Key differences:
// βββββββββββββββββββββββ¬βββββββββββββββββ¬βββββββββββββββββ
// β Feature β #[from] β Manual From β
// βββββββββββββββββββββββΌβββββββββββββββββΌβββββββββββββββββ€
// β Conversion β β β β β
// β ? operator β β β β β
// β Display β β (via #[error])β Manual β
// β Error::source() β β β β β
// β Error chains β Preserved β Lost β
// β Custom logic β β β β β
// β Multiple sources β β β β β
// βββββββββββββββββββββββ΄βββββββββββββββββ΄βββββββββββββββββ
// Use #[from] when:
// - Source error is stored directly
// - Standard conversion is fine
// - You want automatic source tracking
// Use manual From when:
// - Conversion needs custom logic
// - Multiple source types map to one variant
// - Source is transformed during conversion
// - You need to add context
// Common mistake:
#[derive(Debug)]
enum BrokenError {
Io(std::io::Error),
}
impl From<std::io::Error> for BrokenError {
fn from(e: std::io::Error) -> Self {
BrokenError::Io(e)
}
}
// Problem: Error::source() not implemented!
// Error chains break, tools like eyre/anyhow can't show causes
// Fix with thiserror:
#[derive(Debug, Error)]
enum FixedError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
// Now source() works, chains preservedKey insight: The #[from] attribute is about complete error handlingβit generates the From implementation for conversion AND implements Error::source() for proper error chaining. Manual From only handles conversion; you must separately implement Error::source() to preserve error chains. This is why #[from] is preferred for standard error wrapping: it ensures source() is always correct, which is easy to forget when writing manually. Use #[source] when you want source tracking without automatic From, and manual From only when conversion logic is non-trivial or needs context that #[from] can't provide.
