How does thiserror::Error::backtrace enable automatic stack trace capture for error diagnostics?

thiserror::Error::backtrace provides an opt-in mechanism for capturing stack traces when errors are created, using std::backtrace::Backtrace (stable since Rust 1.65) to automatically capture the call stack at the point of error construction. The #[backtrace] attribute in thiserror's derive macro generates code that creates a backtrace field and implements the std::error::Error::provide method, allowing downstream code to request the backtrace via Error::request_ref::<Backtrace>(). This enables rich error diagnostics without manual backtrace management, while keeping the performance impact opt-in—backtraces are only captured when explicitly requested through the error type definition.

Basic Backtrace Capture

use thiserror::Error;
use std::backtrace::Backtrace;
 
#[derive(Error, Debug)]
#[error("Failed to process item {id}")]
struct ProcessError {
    id: u32,
    #[backtrace]  // Automatically captures stack trace
    backtrace: Backtrace,
}
 
fn process_item(id: u32) -> Result<(), ProcessError> {
    // Simulate an error
    Err(ProcessError {
        id,
        backtrace: Backtrace::capture(),  // Captures stack here
    })
}
 
fn main() {
    match process_item(42) {
        Err(e) => {
            println!("Error: {}", e);
            // Backtrace captured at the point of ProcessError creation
            if let Some(bt) = e.request_ref::<Backtrace>() {
                println!("Backtrace:\n{}", bt);
            }
        }
        Ok(()) => {}
    }
}

The #[backtrace] attribute automatically handles backtrace capture in the derived implementation.

Automatic Backtrace with thiserror's Derive Macro

use thiserror::Error;
use std::backtrace::Backtrace;
 
// thiserror generates the backtrace capture code automatically
#[derive(Error, Debug)]
#[error("Database connection failed: {message}")]
pub struct ConnectionError {
    message: String,
    #[backtrace]
    backtrace: Backtrace,
}
 
// Manual implementation would require:
impl std::error::Error for ConnectionError {
    fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
        request.provide_ref(&self.backtrace);
    }
}
 
// thiserror's derive does this automatically with #[backtrace]
 
fn connect_to_database(url: &str) -> Result<(), ConnectionError> {
    // Simulate connection failure
    Err(ConnectionError {
        message: format!("Could not connect to {}", url),
        backtrace: Backtrace::capture(),  // Captured here
    })
}
 
fn main() {
    if let Err(e) = connect_to_database("postgres://localhost/db") {
        eprintln!("Error: {}", e);
        // The backtrace shows where ConnectionError was created
        if let Some(bt) = e.request_ref::<Backtrace>() {
            eprintln!("Stack trace:\n{}", bt);
        }
    }
}

The derive macro generates the provide implementation automatically.

Backtrace Propagation Through Error Chains

use thiserror::Error;
use std::backtrace::Backtrace;
 
#[derive(Error, Debug)]
#[error("Low-level I/O error")]
pub struct IoError {
    #[backtrace]
    backtrace: Backtrace,
}
 
#[derive(Error, Debug)]
#[error("Operation failed")]
pub struct OperationError {
    #[source]
    source: IoError,
    #[backtrace]
    backtrace: Backtrace,
}
 
#[derive(Error, Debug)]
#[error("Service error")]
pub struct ServiceError {
    #[source]
    source: OperationError,
    #[backtrace]
    backtrace: Backtrace,
}
 
fn low_level_operation() -> Result<(), IoError> {
    Err(IoError {
        backtrace: Backtrace::capture(),
    })
}
 
fn operation() -> Result<(), OperationError> {
    low_level_operation().map_err(|e| OperationError {
        source: e,
        backtrace: Backtrace::capture(),  // New backtrace at this level
    })?;
    Ok(())
}
 
fn service_call() -> Result<(), ServiceError> {
    operation().map_err(|e| ServiceError {
        source: e,
        backtrace: Backtrace::capture(),  // And another at service level
    })?;
    Ok(())
}
 
fn main() {
    if let Err(e) = service_call() {
        // Can request backtrace from any level
        if let Some(bt) = e.request_ref::<Backtrace>() {
            println!("Service error backtrace:\n{}", bt);
        }
        
        // Navigate to inner errors
        if let Some(inner) = e.source() {
            if let Some(bt) = inner.request_ref::<Backtrace>() {
                println!("\nOperation error backtrace:\n{}", bt);
            }
        }
    }
}

Each error level can capture its own backtrace for precise diagnostics.

Conditional Backtrace Capture

use thiserror::Error;
use std::backtrace::Backtrace;
 
// Backtrace::capture() respects RUST_BACKTRACE environment variable
// - RUST_BACKTRACE=0: Returns Backtrace::disabled()
// - RUST_BACKTRACE=1: Captures backtrace
// - RUST_BACKTRACE=full: Full backtrace with source lines
 
#[derive(Error, Debug)]
#[error("Configuration error: {message}")]
pub struct ConfigError {
    message: String,
    #[backtrace]
    backtrace: Backtrace,
}
 
fn load_config() -> Result<Config, ConfigError> {
    Err(ConfigError {
        message: "Missing required field".to_string(),
        backtrace: Backtrace::capture(),  // Respects RUST_BACKTRACE
    })
}
 
fn main() {
    // Set RUST_BACKTRACE=1 to see backtrace
    if let Err(e) = load_config() {
        eprintln!("{}", e);
        if let Some(bt) = e.request_ref::<Backtrace>() {
            if !bt.status().is_disabled() {
                eprintln!("Backtrace:\n{}", bt);
            }
        }
    }
}

Backtrace capture respects RUST_BACKTRACE for conditional diagnostics.

Backtrace with Source Errors

use thiserror::Error;
use std::backtrace::Backtrace;
use std::io;
 
#[derive(Error, Debug)]
#[error("Failed to read configuration")]
pub struct ReadConfigError {
    #[source]
    source: io::Error,
    #[backtrace]
    backtrace: Backtrace,
}
 
// When wrapping an error, you can capture backtrace at the wrapping site
impl ReadConfigError {
    pub fn new(source: io::Error) -> Self {
        Self {
            source,
            backtrace: Backtrace::capture(),
        }
    }
}
 
fn read_config_file(path: &str) -> Result<String, ReadConfigError> {
    std::fs::read_to_string(path).map_err(ReadConfigError::new)
}
 
fn main() {
    match read_config_file("nonexistent.toml") {
        Ok(content) => println!("Config: {}", content),
        Err(e) => {
            eprintln!("Error: {}", e);
            // Backtrace from ReadConfigError creation
            if let Some(bt) = e.request_ref::<Backtrace>() {
                eprintln!("Backtrace:\n{}", bt);
            }
            // Source error chain
            if let Some(source) = e.source() {
                eprintln!("Caused by: {}", source);
            }
        }
    }
}

Wrap errors with backtraces to capture the conversion point.

Using provide for Backtrace Access

use std::backtrace::Backtrace;
use std::error::Error;
 
// The provide mechanism (Rust 1.83+) allows requesting backtraces
// from any error type that implements provide
 
fn print_error_with_backtrace(e: &dyn Error) {
    println!("Error: {}", e);
    
    // Request backtrace from the error
    if let Some(bt) = e.request_ref::<Backtrace>() {
        println!("\nBacktrace:\n{}", bt);
    }
    
    // Also check source errors
    let mut source = e.source();
    while let Some(err) = source {
        println!("\nCaused by: {}", err);
        if let Some(bt) = err.request_ref::<Backtrace>() {
            println!("Backtrace:\n{}", bt);
        }
        source = err.source();
    }
}
 
// Works with any error implementing provide
// thiserror's #[backtrace] generates the provide implementation

The request_ref method queries errors for attached backtraces.

Performance Considerations

use thiserror::Error;
use std::backtrace::Backtrace;
 
// Backtrace capture has overhead:
// - Memory allocation for the stack trace
// - Symbol resolution (deferred until display)
// - Stack walking
 
// Use backtraces selectively:
 
#[derive(Error, Debug)]
#[error("Critical error: {message}")]
pub struct CriticalError {
    message: String,
    #[backtrace]
    backtrace: Backtrace,  // Worth it for critical errors
}
 
#[derive(Error, Debug)]
#[error("Minor validation error: {field}")]
pub struct ValidationError {
    field: String,
    // No backtrace - not worth the overhead
}
 
// Validation errors might be frequent and expected
// Critical errors need full diagnostics
 
fn process_user(name: String) -> Result<(), Box<dyn std::error::Error>> {
    if name.is_empty() {
        // Fast path, no backtrace
        return Err(ValidationError { field: "name".into() }.into());
    }
    
    if name.len() > 1000 {
        // This shouldn't happen, include backtrace
        return Err(CriticalError {
            message: "Invalid input size".into(),
            backtrace: Backtrace::capture(),
        }.into());
    }
    
    Ok(())
}

Reserve backtraces for unexpected or critical errors.

Backtrace Status and Display

use thiserror::Error;
use std::backtrace::Backtrace;
 
#[derive(Error, Debug)]
#[error("Internal error")]
pub struct InternalError {
    #[backtrace]
    backtrace: Backtrace,
}
 
fn format_error_with_backtrace(e: &InternalError) -> String {
    let mut output = format!("Error: {}", e);
    
    // Check if backtrace was actually captured
    match e.backtrace.status() {
        std::backtrace::BacktraceStatus::Captured => {
            output.push_str("\n\nBacktrace:\n");
            output.push_str(&format!("{}", e.backtrace));
        }
        std::backtrace::BacktraceStatus::Disabled => {
            output.push_str("\n\n(Backtrace disabled - set RUST_BACKTRACE=1)");
        }
        std::backtrace::BacktraceStatus::Unsupported => {
            output.push_str("\n\n(Backtrace not supported on this platform)");
        }
        _ => {}
    }
    
    output
}

Check backtrace status before displaying to handle disabled cases gracefully.

thiserror's Backtrace Generation

use thiserror::Error;
use std::backtrace::Backtrace;
 
// What thiserror generates for #[backtrace]:
 
#[derive(Error, Debug)]
#[error("Error occurred")]
pub struct MyError {
    #[backtrace]
    backtrace: Backtrace,
}
 
// Approximately generates:
impl std::error::Error for MyError {
    fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
        request.provide_ref(&self.backtrace);
    }
}
 
// And for Display:
impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error occurred")
    }
}
 
// The backtrace field is captured when the error is created
// Not when it's displayed or propagated

thiserror generates the provide implementation automatically.

Combining Backtrace with Other Fields

use thiserror::Error;
use std::backtrace::Backtrace;
 
#[derive(Error, Debug)]
#[error("Failed to process {item}: {reason}")]
pub struct ProcessingError {
    item: String,
    reason: String,
    #[source]
    source: Option<std::io::Error>,
    #[backtrace]
    backtrace: Backtrace,
}
 
impl ProcessingError {
    pub fn new(item: String, reason: String) -> Self {
        Self {
            item,
            reason,
            source: None,
            backtrace: Backtrace::capture(),
        }
    }
    
    pub fn with_source(item: String, reason: String, source: std::io::Error) -> Self {
        Self {
            item,
            reason,
            source: Some(source),
            backtrace: Backtrace::capture(),
        }
    }
}
 
fn process_files(files: &[String]) -> Result<(), ProcessingError> {
    for file in files {
        if file.ends_with(".tmp") {
            return Err(ProcessingError::new(
                file.clone(),
                "Temporary files not allowed".to_string(),
            ));
        }
    }
    Ok(())
}

Backtrace fields integrate naturally with other error information.

Error Inspection Utility

use std::backtrace::Backtrace;
use std::error::Error;
 
fn inspect_error_chain(e: &dyn Error) {
    let mut depth = 0;
    let mut current: Option<&dyn Error> = Some(e);
    
    while let Some(err) = current {
        println!("{}{}", "  ".repeat(depth), err);
        
        // Check for backtrace at each level
        if let Some(bt) = err.request_ref::<Backtrace>() {
            println!("{}Backtrace:", "  ".repeat(depth));
            for (i, frame) in bt.frames().iter().enumerate().take(5) {
                println!("{}  [{}] {:?}", "  ".repeat(depth), i, frame);
            }
        }
        
        current = err.source();
        depth += 1;
    }
}
 
// Usage with thiserror-generated errors:
#[derive(Error, Debug)]
#[error("Level 1 error")]
struct Level1 {
    #[backtrace]
    backtrace: Backtrace,
}
 
#[derive(Error, Debug)]
#[error("Level 2 error")]
struct Level2 {
    #[source]
    source: Level1,
    #[backtrace]
    backtrace: Backtrace,
}

Navigate error chains and extract backtraces at each level.

Environment-Based Backtrace Control

use thiserror::Error;
use std::backtrace::Backtrace;
 
// RUST_BACKTRACE environment variable controls capture:
// - Not set: Backtrace::capture() returns disabled backtrace
// - RUST_BACKTRACE=0: Same as not set
// - RUST_BACKTRACE=1: Captures backtrace
// - RUST_BACKTRACE=full: Full backtrace with source lines
 
// Programmatic control (Rust 1.65+):
fn should_capture_backtrace() -> bool {
    std::env::var("RUST_BACKTRACE").is_ok()
}
 
#[derive(Error, Debug)]
#[error("Service error: {message}")]
pub struct ServiceError {
    message: String,
    #[backtrace]
    backtrace: Backtrace,
}
 
// Alternative: Manual capture for conditional diagnostics
fn create_service_error(message: String) -> ServiceError {
    ServiceError {
        message,
        backtrace: Backtrace::capture(),  // Respects RUST_BACKTRACE
    }
}
 
// Or use Backtrace::force_capture() to capture regardless of env
fn create_verbose_error(message: String) -> ServiceError {
    ServiceError {
        message,
        backtrace: Backtrace::force_capture(),
    }
}

Control backtrace capture through environment variables or explicit methods.

Real-World Error Type Design

use thiserror::Error;
use std::backtrace::Backtrace;
 
// Layered error design with backtraces at key points
 
#[derive(Error, Debug)]
#[error("Database query failed")]
pub struct QueryError {
    query: String,
    #[source]
    source: DatabaseError,
    #[backtrace]
    backtrace: Backtrace,
}
 
#[derive(Error, Debug)]
#[error("Database connection error")]
pub struct DatabaseError {
    #[source]
    source: std::io::Error,
    #[backtrace]
    backtrace: Backtrace,
}
 
#[derive(Error, Debug)]
#[error("Application error")]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] QueryError),
    
    #[error("Validation error: {field}")]
    Validation { field: String },  // No backtrace for expected errors
}
 
// Usage in application:
fn query_database() -> Result<(), QueryError> {
    // Simulate database failure
    Err(QueryError {
        query: "SELECT * FROM users".into(),
        source: DatabaseError {
            source: std::io::Error::new(std::io::ErrorKind::ConnectionReset, "connection lost"),
            backtrace: Backtrace::capture(),
        },
        backtrace: Backtrace::capture(),
    })
}

Strategic backtrace placement provides diagnostic value at critical layers.

Synthesis

How #[backtrace] works:

Step Action
1 Add #[backtrace] attribute to a Backtrace field
2 thiserror generates provide implementation
3 When error is created, Backtrace::capture() captures the stack
4 Downstream code uses request_ref::<Backtrace>() to access
5 Backtrace displays with RUST_BACKTRACE environment control

Key benefits:

  • Automatic stack capture at error creation point
  • Opt-in performance cost (only when used)
  • Integration with std::error::Error::provide
  • Works through error chains
  • Respects environment variables for conditional capture

When to use backtraces:

Scenario Recommendation
Expected errors (validation, not found) Skip backtrace
Unexpected errors (system failures) Include backtrace
Debug builds only Use RUST_BACKTRACE
Production critical errors Consider always capturing

Key insight: thiserror::Error::backtrace provides zero-boilerplate stack trace capture for errors by generating the provide implementation automatically. The #[backtrace] attribute tells thiserror to include the backtrace field in the error's provider, allowing Error::request_ref::<Backtrace>() to retrieve it. This enables rich error diagnostics without manual backtrace plumbing—just add #[backtrace] to a field and the backtrace is captured when the error is created. The performance impact is controlled by RUST_BACKTRACE, making it safe to include backtraces in production code where the environment variable can disable them. Use backtraces for unexpected errors where knowing the call stack helps diagnose root causes, and skip them for expected errors like validation failures where the context is clear from the error message.