What is the difference between thiserror::Error::backtrace and std::backtrace::Backtrace for error diagnostics?

std::backtrace::Backtrace captures a stack trace at a specific point in code, while thiserror::Error::backtrace is a derive macro attribute that automatically adds backtrace support to error types. The key difference is that std::backtrace::Backtrace is the underlying mechanism for capturing stack traces, while thiserror::Error::backtrace integrates this capture into the error type automatically—capturing the backtrace when the error is created and making it available through the Error trait's provide method. This integration ensures backtraces are captured at the point of error creation, not propagation.

std::backtrace::Backtrace Basics

use std::backtrace::Backtrace;
 
fn capture_backtrace() {
    // Capture current stack trace
    let backtrace = Backtrace::capture();
    
    println!("Backtrace:\n{}", backtrace);
    
    // Backtrace captures the call stack at this point
    // Including function names, file names, and line numbers
}
 
fn main() {
    capture_backtrace();
}

Backtrace::capture() creates a snapshot of the current call stack.

Backtrace Capture Modes

use std::backtrace::Backtrace;
 
fn capture_modes() {
    // RUST_BACKTRACE=1 enables capture
    let backtrace = Backtrace::capture();
    
    // Check the capture status
    match backtrace.status() {
        std::backtrace::BacktraceStatus::Supported => {
            println!("Backtrace captured: {}", backtrace);
        }
        std::backtrace::BacktraceStatus::Disabled => {
            println!("Backtraces are disabled (RUST_BACKTRACE=0)");
        }
        std::backtrace::BacktraceStatus::Unsupported => {
            println!("Backtraces not supported on this platform");
        }
        _ => {}
    }
}

Backtrace capture requires RUST_BACKTRACE=1 environment variable.

Manual Backtrace in Errors

use std::backtrace::Backtrace;
use std::error::Error;
use std::fmt;
 
#[derive(Debug)]
struct MyError {
    message: String,
    backtrace: Backtrace,
}
 
impl MyError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            backtrace: Backtrace::capture(),
        }
    }
}
 
impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)
    }
}
 
impl Error for MyError {
    fn provide<'a>(&'a self, demand: &mut std::any::Demand<'a>) {
        demand.provide(&self.backtrace);
    }
}
 
fn use_manual_error() -> Result<(), MyError> {
    Err(MyError::new("Something went wrong"))
}

Manually storing Backtrace captures it at error creation time.

thiserror::Error::backtrace Attribute

use thiserror::Error;
 
#[derive(Error, Debug)]
#[error("{message}")]
struct AppError {
    message: String,
    #[backtrace]  // Automatically captures backtrace
    backtrace: std::backtrace::Backtrace,
}
 
fn use_thiserror_backtrace() -> Result<(), AppError> {
    Err(AppError {
        message: "Operation failed".to_string(),
        backtrace: std::backtrace::Backtrace::capture(),
    })
}
 
// The derive macro handles:
// 1. Capturing the backtrace in the constructor
// 2. Implementing Error::provide to expose the backtrace

#[backtrace] attribute integrates backtrace capture into the error type.

Automatic Backtrace Capture with thiserror

use thiserror::Error;
 
#[derive(Error, Debug)]
#[error("Database error: {message}")]
struct DatabaseError {
    message: String,
    #[backtrace]
    backtrace: std::backtrace::Backtrace,
}
 
impl DatabaseError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            backtrace: std::backtrace::Backtrace::capture(),
        }
    }
}
 
fn query_database() -> Result<String, DatabaseError> {
    Err(DatabaseError::new("Connection refused"))
}
 
fn main() {
    std::env::set_var("RUST_BACKTRACE", "1");
    
    match query_database() {
        Err(e) => {
            eprintln!("Error: {}", e);
            // Backtrace captured at DatabaseError::new()
            if let Some(bt) = std::error::request_ref::<std::backtrace::Backtrace>(&e) {
                eprintln!("Backtrace:\n{}", bt);
            }
        }
        Ok(_) => {}
    }
}

The backtrace captures where DatabaseError::new() was called.

Error Trait Integration

use std::backtrace::Backtrace;
use std::error::Error;
use std::any::Demand;
 
#[derive(Debug)]
struct CustomError {
    message: String,
    backtrace: Backtrace,
}
 
impl Error for CustomError {
    fn provide<'a>(&'a self, demand: &mut Demand<'a>) {
        demand.provide(&self.backtrace);
    }
}
 
fn access_backtrace() {
    let error = CustomError {
        message: "test".to_string(),
        backtrace: Backtrace::capture(),
    };
    
    // Access backtrace through Error::provide
    let backtrace: &Backtrace = std::error::request_ref(&error).unwrap();
    println!("Backtrace:\n{}", backtrace);
}

The provide method allows requesting backtrace from any error type.

Backtrace Source Tracking

use std::backtrace::Backtrace;
use thiserror::Error;
 
#[derive(Error, Debug)]
#[error("IO error: {0}")]
struct IoError(#[source] std::io::Error);
 
#[derive(Error, Debug)]
#[error("Config error: {message}")]
struct ConfigError {
    message: String,
    #[source]
    source: IoError,
    #[backtrace]
    backtrace: Backtrace,
}
 
// When propagating errors, source backtraces are preserved
fn load_config() -> Result<String, ConfigError> {
    std::fs::read_to_string("config.txt")
        .map_err(|e| ConfigError {
            message: "Failed to load config".to_string(),
            source: IoError(e),
            backtrace: Backtrace::capture(), // Captured here
        })
}

The backtrace shows where the error was created, not propagated.

Comparing Backtrace Points

use std::backtrace::Backtrace;
use thiserror::Error;
 
#[derive(Error, Debug)]
#[error("{message}")]
struct ErrorA {
    message: String,
    #[backtrace]
    backtrace: Backtrace,
}
 
fn inner_function() -> Result<(), ErrorA> {
    // Backtrace captured HERE
    Err(ErrorA {
        message: "Inner error".to_string(),
        backtrace: Backtrace::capture(),
    })
}
 
fn middle_function() -> Result<(), ErrorA> {
    inner_function()?;  // Propagates, doesn't capture new backtrace
    Ok(())
}
 
fn outer_function() -> Result<(), ErrorA> {
    middle_function()?;  // Propagates, doesn't capture new backtrace
    Ok(())
}
 
fn demonstrate_capture_point() {
    std::env::set_var("RUST_BACKTRACE", "1");
    
    if let Err(e) = outer_function() {
        // Backtrace shows inner_function, not outer_function
        if let Some(bt) = std::error::request_ref::<Backtrace>(&e) {
            println!("Backtrace captured at error creation:\n{}", bt);
        }
    }
}

Backtraces are captured at error creation, showing the origin.

Multiple Backtrace Sources

use std::backtrace::Backtrace;
use thiserror::Error;
 
#[derive(Error, Debug)]
#[error("Layer 1: {message}")]
struct ErrorLayer1 {
    message: String,
    #[backtrace]
    backtrace: Backtrace,
}
 
#[derive(Error, Debug)]
#[error("Layer 2: {message}")]
#[source]
struct ErrorLayer2 {
    message: String,
    #[source]
    source: ErrorLayer1,
    #[backtrace]
    backtrace: Backtrace,
}
 
fn error_with_source() -> Result<(), ErrorLayer2> {
    let layer1 = ErrorLayer1 {
        message: "Original error".to_string(),
        backtrace: Backtrace::capture(),
    };
    
    Err(ErrorLayer2 {
        message: "Wrapped error".to_string(),
        source: layer1,
        backtrace: Backtrace::capture(),
    })
}
 
fn access_all_backtraces() {
    std::env::set_var("RUST_BACKTRACE", "1");
    
    if let Err(e) = error_with_source() {
        // Access outer error's backtrace
        if let Some(bt) = std::error::request_ref::<Backtrace>(&e) {
            println!("Outer backtrace:\n{}", bt);
        }
        
        // Access source error's backtrace
        if let Some(source) = e.source() {
            if let Some(bt) = std::error::request_ref::<Backtrace>(source) {
                println!("Source backtrace:\n{}", bt);
            }
        }
    }
}

Each error layer can have its own backtrace showing where it was created.

Backtrace with Source Errors

use thiserror::Error;
use std::backtrace::Backtrace;
 
#[derive(Error, Debug)]
#[error("Service error: {message}")]
struct ServiceError {
    message: String,
    #[source]
    source: std::io::Error, // std::io::Error has no backtrace
    #[backtrace]
    backtrace: Backtrace, // Captures where ServiceError was created
}
 
fn read_data() -> Result<String, ServiceError> {
    std::fs::read_to_string("data.txt")
        .map_err(|e| ServiceError {
            message: "Failed to read data".to_string(),
            source: e,
            backtrace: Backtrace::capture(),
        })
}

The backtrace shows where ServiceError wraps the source error.

Environment Configuration

use std::backtrace::Backtrace;
 
fn backtrace_env() {
    // RUST_BACKTRACE=0: Backtrace::capture() returns disabled backtrace
    // RUST_BACKTRACE=1: Captures full backtrace
    // RUST_BACKTRACE=full: Same as 1
    // RUST_LIB_BACKTRACE=1: Enables library-level backtrace capture
    
    let backtrace = Backtrace::capture();
    
    // Without RUST_BACKTRACE set:
    // backtrace.status() == BacktraceStatus::Disabled
    
    // With RUST_BACKTRACE=1:
    // backtrace.status() == BacktraceStatus::Supported
}
 
fn print_backtrace() {
    std::env::set_var("RUST_BACKTRACE", "1");
    
    let bt = Backtrace::capture();
    println!("{}", bt);
    
    // Typical output:
    //    0: rust_begin_unwind
    //    1: core::panicking::panic_fmt
    //    2: my_function::at::file.rs:10:5
    //    ...
}

Backtraces require environment configuration to capture.

Backtrace Resolution

use std::backtrace::Backtrace;
 
fn backtrace_resolution() {
    std::env::set_var("RUST_BACKTRACE", "1");
    
    let backtrace = Backtrace::capture();
    
    // Backtraces can be resolved or unresolved
    // Resolved: function names and line numbers are resolved
    // Unresolved: only addresses are available
    
    // In debug mode: symbols are resolved
    // In release mode: may need debug symbols
    
    // Force resolution (if needed):
    let resolved = backtrace.resolve();
    println!("Resolved: {}", resolved);
}

Backtrace resolution depends on debug symbols and platform support.

thiserror vs std::backtrace Summary

use std::backtrace::Backtrace;
use thiserror::Error;
 
// std::backtrace::Backtrace: The mechanism
// - Captures stack trace at call site
// - Requires RUST_BACKTRACE=1
// - Must be manually stored and accessed
 
// thiserror::Error::backtrace: The integration
// - Derive macro attribute
// - Automatically stores backtrace
// - Implements Error::provide for access
// - Syntactic sugar over manual implementation
 
#[derive(Error, Debug)]
#[error("{message}")]
struct AutoBacktrace {
    message: String,
    #[backtrace]
    backtrace: Backtrace, // thiserror handles capture and access
}
 
#[derive(Debug)]
struct ManualBacktrace {
    message: String,
    backtrace: Backtrace, // Must manually capture and implement provide
}
 
impl ManualBacktrace {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            backtrace: Backtrace::capture(),
        }
    }
}
 
impl std::error::Error for ManualBacktrace {
    fn provide<'a>(&'a self, demand: &mut std::any::Demand<'a>) {
        demand.provide(&self.backtrace);
    }
}

thiserror::Error::backtrace is syntactic sugar over manual backtrace handling.

Real-World Example: Application Error Handling

use thiserror::Error;
use std::backtrace::Backtrace;
 
#[derive(Error, Debug)]
pub enum AppError {
    #[error("Configuration error: {message}")]
    Config {
        message: String,
        #[backtrace]
        backtrace: Backtrace,
    },
    
    #[error("Database error: {message}")]
    Database {
        message: String,
        #[source]
        source: sqlx::Error,
        #[backtrace]
        backtrace: Backtrace,
    },
    
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}
 
impl AppError {
    pub fn config(message: impl Into<String>) -> Self {
        Self::Config {
            message: message.into(),
            backtrace: Backtrace::capture(),
        }
    }
    
    pub fn database(message: impl Into<String>, source: sqlx::Error) -> Self {
        Self::Database {
            message: message.into(),
            source,
            backtrace: Backtrace::capture(),
        }
    }
}
 
fn load_config() -> Result<Config, AppError> {
    // Backtrace captured here
    Err(AppError::config("Missing configuration file"))
}
 
fn query_database() -> Result<Data, AppError> {
    // Database error wrapped here with backtrace
    // Err(AppError::database("Query failed", db_error))
    todo!()
}

Application errors automatically capture backtraces at creation.

Real-World Example: Error Reporting

use thiserror::Error;
use std::backtrace::Backtrace;
 
fn report_error(error: &dyn std::error::Error) {
    eprintln!("Error: {}", error);
    
    // Print source chain
    let mut source = error.source();
    while let Some(s) = source {
        eprintln!("Caused by: {}", s);
        source = s.source();
    }
    
    // Print backtrace if available
    if let Some(bt) = std::error::request_ref::<Backtrace>(error) {
        eprintln!("\nBacktrace:\n{}", bt);
    }
}
 
#[derive(Error, Debug)]
#[error("Processing failed: {message}")]
struct ProcessingError {
    message: String,
    #[backtrace]
    backtrace: Backtrace,
}
 
fn main_example() {
    std::env::set_var("RUST_BACKTRACE", "1");
    
    let error = ProcessingError {
        message: "Data validation failed".to_string(),
        backtrace: Backtrace::capture(),
    };
    
    report_error(&error);
}

Error reporting can request backtraces from any error type.

Real-World Example: Error Aggregation

use std::backtrace::Backtrace;
use thiserror::Error;
 
#[derive(Error, Debug)]
#[error("Multiple errors occurred")]
struct AggregateError {
    errors: Vec<Box<dyn std::error::Error + Send + Sync>>,
    #[backtrace]
    backtrace: Backtrace,
}
 
impl AggregateError {
    fn new(errors: Vec<Box<dyn std::error::Error + Send + Sync>>) -> Self {
        Self {
            errors,
            backtrace: Backtrace::capture(),
        }
    }
}
 
fn process_batch(items: &[Data]) -> Result<(), AggregateError> {
    let mut errors = Vec::new();
    
    for item in items {
        if let Err(e) = process_item(item) {
            errors.push(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
        }
    }
    
    if !errors.is_empty() {
        // Backtrace captured where aggregation happens
        return Err(AggregateError::new(errors));
    }
    
    Ok(())
}

Aggregate errors capture backtraces where multiple errors combine.

Synthesis

Key differences:

Aspect std::backtrace::Backtrace thiserror::Error::backtrace
What it is A type for capturing stack traces A derive macro attribute
Purpose Capture call stack Integrate backtrace into error types
Capture point Where Backtrace::capture() is called Where error struct is created
Access Direct reference or via Error::provide Automatically via Error::provide
Boilerplate Manual storage and implementation Automatic handling

Backtrace lifecycle:

Stage Action
Creation Backtrace::capture() captures current stack
Storage Stored in error struct as field
Propagation Carried through error chain
Access Via std::error::request_ref or direct reference

When to use each:

Scenario Choice
Custom error type with thiserror #[backtrace] attribute
Manual error implementation Backtrace field + Error::provide
Debug/panic backtraces Backtrace::capture() directly
Third-party error wrapping Add backtrace field when wrapping

Key insight: std::backtrace::Backtrace is the primitive for capturing stack traces, while thiserror::Error::backtrace is a derive macro that integrates backtrace capture into error types. The macro generates code that stores a Backtrace captured at error creation and implements Error::provide to expose it. This ensures backtraces show where errors originate rather than where they're handled—the capture happens when new() or the struct constructor runs, preserving the error's origin. For applications, this means you can trace errors back to their source through the error chain, with each error potentially carrying its own backtrace showing where in the code it was created.