How does thiserror::Error::from derive implementation differ from manual From trait implementations?

The thiserror crate's #[source] attribute with #[from] automatically generates both the source() method for the Error trait and a From implementation that converts the source error into your error type, eliminating boilerplate while ensuring consistency between error conversion and error source reporting. Manual From implementations require writing both the conversion logic and the error source method separately, which can lead to inconsistencies if the source error is wrapped differently in the From implementation versus what source() returns. The derive macro also handles edge cases like multiple source fields and conditional compilation, producing the same correct code that you would write manually but with less room for mistakes.

Manual From Implementation

use std::error::Error;
use std::fmt;
 
// Define a custom error type
#[derive(Debug)]
pub enum DataStoreError {
    IoError(std::io::Error),
    ParseError(serde_json::Error),
    NotFound { id: String },
}
 
// Manual From implementation for io::Error
impl From<std::io::Error> for DataStoreError {
    fn from(err: std::io::Error) -> Self {
        DataStoreError::IoError(err)
    }
}
 
// Manual From implementation for serde_json::Error
impl From<serde_json::Error> for DataStoreError {
    fn from(err: serde_json::Error) -> Self {
        DataStoreError::ParseError(err)
    }
}
 
// Manual Error implementation
impl Error for DataStoreError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            DataStoreError::IoError(e) => Some(e),
            DataStoreError::ParseError(e) => Some(e),
            DataStoreError::NotFound { .. } => None,
        }
    }
}
 
impl fmt::Display for DataStoreError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DataStoreError::IoError(e) => write!(f, "IO error: {}", e),
            DataStoreError::ParseError(e) => write!(f, "Parse error: {}", e),
            DataStoreError::NotFound { id } => write!(f, "Not found: {}", id),
        }
    }
}

Manual implementations require writing each From impl and the Error::source() method separately.

thiserror Derive with #[from]

use thiserror::Error;
 
// The same error type with thiserror derive
#[derive(Debug, Error)]
pub enum DataStoreError {
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),
    
    #[error("Parse error: {0}")]
    ParseError(#[from] serde_json::Error),
    
    #[error("Not found: {id}")]
    NotFound { id: String },
}
 
// The #[from] attribute generates:
// 1. From<std::io::Error> for DataStoreError
// 2. source() method returning the error
 
fn example() -> Result<(), DataStoreError> {
    // ? operator works because of generated From impl
    let data = std::fs::read_to_string("data.json")?;
    let parsed: serde_json::Value = serde_json::from_str(&data)?;
    Ok(())
}

The #[from] attribute generates both the From impl and the source() method automatically.

Generated Code Comparison

use thiserror::Error;
use std::error::Error;
 
// With thiserror:
#[derive(Debug, Error)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),
}
 
// What thiserror generates (approximately):
impl From<sqlx::Error> for AppError {
    fn from(err: sqlx::Error) -> Self {
        AppError::Database(err)
    }
}
 
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Database(err) => Some(err),
        }
    }
}
 
// Without #[from], you'd write:
impl From<sqlx::Error> for AppError {
    fn from(err: sqlx::Error) -> Self {
        AppError::Database(err)
    }
}
 
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Database(err) => Some(err),
        }
    }
}

The generated code is identical to what you'd write manually, but with less boilerplate.

Automatic Source Method

use thiserror::Error;
use std::error::Error;
 
#[derive(Debug, Error)]
pub enum NetworkError {
    #[error("Connection failed: {0}")]
    Connection(#[from] std::io::Error),
    
    #[error("DNS resolution failed")]
    DnsError,
    
    #[error("Timeout after {ms}ms")]
    Timeout { ms: u64, #[source] inner: std::io::Error },
}
 
// #[from] on Connection generates From<std::io::Error>
// #[source] on Timeout's inner field generates source() only
 
fn demonstrate_source() {
    let err = NetworkError::Connection(std::io::Error::new(
        std::io::ErrorKind::ConnectionRefused,
        "connection refused"
    ));
    
    // source() returns the inner error
    let source = err.source();
    assert!(source.is_some());
    
    // For Timeout variant, source() returns inner field
    let timeout_err = NetworkError::Timeout {
        ms: 1000,
        inner: std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout"),
    };
    let source = timeout_err.source();
    assert!(source.is_some());
}

#[from] generates both From and source(); #[source] generates only source().

Consistency Between From and Source

use thiserror::Error;
use std::error::Error;
 
// Manual implementation can have inconsistencies:
#[derive(Debug)]
pub enum BadError {
    Io(std::io::Error),
}
 
// From impl wraps the error
impl From<std::io::Error> for BadError {
    fn from(err: std::io::Error) -> Self {
        BadError::Io(err)
    }
}
 
// But source() might return something different by mistake!
impl Error for BadError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            // Bug: Returning None instead of the actual error
            BadError::Io(_) => None,
        }
    }
}
 
// With thiserror, this can't happen:
#[derive(Debug, Error)]
pub enum GoodError {
    #[error("IO error")]
    Io(#[from] std::io::Error),
}
 
// Generated code is always consistent:
// - From wraps in Io variant
// - source() returns the wrapped error

thiserror ensures the From impl and source() method stay consistent.

Multiple Source Fields

use thiserror::Error;
 
// Error: Cannot have multiple #[from] fields
#[derive(Debug, Error)]
pub enum MultiSourceError {
    #[error("Error A: {0}")]
    ErrorA(#[from] std::io::Error),
    
    #[error("Error B: {0}")]
    // #[from] ErrorB(OtherError),  // Would cause compile error
    // Only one #[from] per variant allowed
    ErrorB(std::fmt::Error),  // Use #[source] instead
}
 
// Correct approach with multiple sources:
#[derive(Debug, Error)]
pub enum ProperMultiError {
    #[error("IO failed")]
    Io(#[from] std::io::Error),  // Generates From impl
    
    #[error("Format failed")]
    Format(#[source] std::fmt::Error),  // Only source(), no From
}
 
// Only one From impl can exist for each type
// thiserror enforces this with #[from] restrictions

Only one #[from] per variant; use #[source] for additional error fields.

Struct Variants with From

use thiserror::Error;
use std::error::Error;
 
#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("Failed to load config from {path}")]
    Load {
        path: String,
        #[from]
        source: std::io::Error,
    },
    
    #[error("Invalid config at {path}: {message}")]
    InvalidConfig {
        path: String,
        message: String,
        #[source]
        underlying: serde_json::Error,  // source() only, no From
    },
}
 
// Load variant has #[from] on source field:
// - Generates From<std::io::Error> for ConfigError
// - source() returns the underlying error
 
fn load_config() -> Result<Config, ConfigError> {
    let path = "config.json";
    let data = std::fs::read_to_string(path)?;  // Uses generated From impl
    Ok(Config { /* ... */ })
}

#[from] works on struct variant fields, not just tuple variants.

Explicit From for Custom Conversion

use thiserror::Error;
use std::error::Error;
 
#[derive(Debug, Error)]
pub enum RepositoryError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),
    
    #[error("Entity not found: {id}")]
    NotFound { id: i64 },
}
 
// Sometimes you need custom From logic:
// Manual impl for custom conversion
impl From<i64> for RepositoryError {
    fn from(id: i64) -> Self {
        RepositoryError::NotFound { id }
    }
}
 
// Or custom wrapping when default From doesn't match:
impl From<String> for RepositoryError {
    fn from(msg: String) -> Self {
        // Custom conversion logic
        RepositoryError::NotFound { id: 0 }
    }
}
 
// thiserror's #[from] can't handle custom logic
// Use manual impl when conversion needs transformation

Use manual From when you need custom conversion logic beyond simple wrapping.

From with Error Types That Don't Implement Error

use thiserror::Error;
 
// Error: #[from] requires the source type to impl Error
#[derive(Debug, Error)]
pub enum ConversionError {
    // This would fail if String doesn't impl Error
    // #[error("Failed: {0}")]
    // StringError(#[from] String),  // String doesn't impl Error!
    
    // Correct: Use #[source] with manual From for types that don't impl Error
    #[error("Invalid value: {value}")]
    InvalidValue {
        value: String,
        #[source]  // Only generates source()
        source: std::convert::Infallible,  // Placeholder, must impl Error
    },
}
 
// Manual From for types that don't impl Error:
impl From<String> for ConversionError {
    fn from(value: String) -> Self {
        ConversionError::InvalidValue {
            value,
            source: std::convert::Infallible,  // Won't compile, illustration
        }
    }
}

#[from] requires the source type to implement Error; for other types, use manual From.

Transparency with from

use thiserror::Error;
use std::error::Error;
 
// #[error(transparent)] forwards Display and source:
#[derive(Debug, Error)]
pub enum WrapperError {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    
    #[error(transparent)]
    Other(#[from] Box<dyn Error + Send + Sync>),
}
 
// transparent + from generates:
// 1. From impl for each variant
// 2. source() that returns the inner error
// 3. Display that forwards to inner error's Display
 
fn wrapper_example() -> Result<(), WrapperError> {
    let data = std::fs::read_to_string("file.txt")?;  // IoError -> WrapperError
    Ok(())
}
 
// This is equivalent to manual:
impl Error for WrapperError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            WrapperError::Io(e) => Some(e),
            WrapperError::Other(e) => Some(e.as_ref()),
        }
    }
}
 
impl std::fmt::Display for WrapperError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            WrapperError::Io(e) => e.fmt(f),
            WrapperError::Other(e) => e.fmt(f),
        }
    }
}

#[error(transparent)] combined with #[from] creates transparent error forwarding.

Backtrace Integration

use thiserror::Error;
use std::backtrace::Backtrace;
 
#[derive(Debug, Error)]
pub enum DetailedError {
    #[error("IO error: {0}")]
    Io {
        #[from]
        source: std::io::Error,
        backtrace: Backtrace,
    },
    
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
}
 
// With #[from], thiserror generates From impl that captures backtrace:
impl From<std::io::Error> for DetailedError {
    fn from(source: std::io::Error) -> Self {
        DetailedError::Io {
            source,
            backtrace: Backtrace::capture(),
        }
    }
}
 
// Without #[from], manual impl would need to:
impl From<std::num::ParseIntError> for DetailedError {
    fn from(source: std::num::ParseIntError) -> Self {
        // For tuple variants, can't add backtrace
        // Would need to be struct variant
        DetailedError::Parse(source)
    }
}

#[from] on struct variants can capture additional context like backtraces.

When Manual From Is Better

use thiserror::Error;
 
#[derive(Debug, Error)]
pub enum ServiceError {
    #[error("Connection to {host}:{port} failed: {source}")]
    ConnectionFailed {
        host: String,
        port: u16,
        #[source]
        source: std::io::Error,
    },
}
 
// Manual From when you need to extract/transform data:
impl From<std::io::Error> for ServiceError {
    fn from(err: std::io::Error) -> Self {
        // Can't use #[from] because we need context:
        // Where did the error come from?
        ServiceError::ConnectionFailed {
            host: "unknown".to_string(),
            port: 0,
            source: err,
        }
    }
}
 
// Better: Provide context explicitly
fn connect(host: &str, port: u16) -> Result<Connection, ServiceError> {
    std::net::TcpStream::connect((host, port))
        .map_err(|e| ServiceError::ConnectionFailed {
            host: host.to_string(),
            port,
            source: e,
        })?;
    Ok(Connection {})
}
 
// Can't do this with #[from] because context isn't available

Use manual From when conversion requires context that isn't in the error type.

From for Third-Party Error Types

use thiserror::Error;
use std::error::Error;
 
// Implementing From for third-party errors:
#[derive(Debug, Error)]
pub enum AppError {
    #[error("Database error")]
    Database(#[from] sqlx::Error),
    
    #[error("HTTP error")]
    Http(#[from] reqwest::Error),
    
    #[error("Serialization error")]
    Serialize(#[from] serde_json::Error),
}
 
// thiserror generates From impl for each:
// impl From<sqlx::Error> for AppError { ... }
// impl From<reqwest::Error> for AppError { ... }
// impl From<serde_json::Error> for AppError { ... }
 
// This enables the ? operator:
async fn fetch_data() -> Result<String, AppError> {
    let client = reqwest::Client::new();
    let response = client.get("https://api.example.com").send().await?;
    let text = response.text().await?;
    let data: serde_json::Value = serde_json::from_str(&text)?;
    Ok(data.to_string())
}

#[from] works with any error type that implements Error.

Trait Bounds with From

use thiserror::Error;
use std::error::Error;
 
// thiserror handles generic bounds correctly:
#[derive(Debug, Error)]
pub enum GenericError<E: Error + 'static> {
    #[error("Wrapped error: {0}")]
    Wrapped(#[from] E),
}
 
// For manual impl, you'd write:
impl<E: Error + 'static> From<E> for GenericError<E> {
    fn from(err: E) -> Self {
        GenericError::Wrapped(err)
    }
}
 
impl<E: Error + 'static> Error for GenericError<E> {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            GenericError::Wrapped(e) => Some(e),
        }
    }
}
 
// thiserror derives the correct bounds automatically

thiserror generates correct trait bounds for generic error types.

Comparison Summary

// Manual From impl:
// + Full control over conversion logic
// + Can add custom context during conversion
// + Works with non-Error types
// - More boilerplate
// - Can be inconsistent with source()
// - Easy to forget to impl source()
 
// thiserror #[from]:
// + Generates consistent From and source()
// + Less boilerplate
// + Automatic handling of edge cases
// + Works well with transparent
// - Limited to simple wrapping (no transformation)
// - Requires source type to impl Error
// - Can't add context not in the error
 
// When to use manual From:
// - Need custom transformation during conversion
// - Source type doesn't impl Error
// - Need to add context (host, port, etc.)
// - Complex conversion logic
 
// When to use #[from]:
// - Simple error wrapping
// - Source type impls Error
// - Want consistency between From and source()
// - Standard error propagation with ?

Choose based on whether you need simple wrapping or custom conversion logic.

Synthesis

What #[from] generates:

#[derive(Debug, Error)]
pub enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}
 
// Generates equivalent to:
impl From<std::io::Error> for MyError {
    fn from(err: std::io::Error) -> Self {
        MyError::Io(err)
    }
}
 
impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            MyError::Io(err) => Some(err),
        }
    }
}

Key differences from manual implementation:

// Manual From:
impl From<std::io::Error> for MyError {
    fn from(err: std::io::Error) -> Self {
        MyError::Io(err)  // Custom logic possible here
    }
}
 
// #[from] attribute:
// - Always wraps in specified variant
// - Always generates matching source()
// - No custom logic possible
// - Guaranteed consistency
 
// #[source] attribute (no From):
#[derive(Debug, Error)]
pub enum MyError {
    #[error("IO error: {0}")]
    Io(#[source] std::io::Error),  // Only source(), no From impl
}
 
// Use when you don't want automatic From conversion

Consistency guarantee:

// Manual implementation can have bugs:
impl From<std::io::Error> for BuggyError {
    fn from(err: std::io::Error) -> Self {
        BuggyError::Wrapped(err)  // Wraps in Wrapped variant
    }
}
 
impl Error for BuggyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            BuggyError::Wrapped(err) => Some(err),
            BuggyError::Other(_) => None,
            // Bug: Forgot to return source for Some cases
        }
    }
}
 
// thiserror guarantees consistency:
// - From wraps in the variant with #[from]
// - source() returns exactly that field
// - No possibility of mismatch

Key insight: thiserror's #[from] attribute eliminates boilerplate by generating both the From implementation and the Error::source() method simultaneously, guaranteeing they stay consistent. Manual From implementations require writing both pieces separately, creating opportunities for subtle bugs where From wraps an error one way but source() reports it differently. The derive macro produces identical code to what you'd write manually for simple cases, but it can't handle transformations—you still need manual From when converting requires extracting context or transforming the error. Use #[from] for straightforward error wrapping where the source error is simply stored in a field; use manual From when you need to add context, transform data, or implement From for types that don't implement Error. The #[source] attribute provides a middle ground: it generates only source() without From, useful when you want error chaining without automatic conversion.