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 preserved

Key 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.