How does thiserror::Error::backtrace support capturing stack traces for error types?
thiserror supports backtrace capture through the #[source] attribute with backtrace field derivation and the #[backtrace] attribute on source fields, enabling automatic propagation of stack traces through error chains without manual implementation of the std::any::Provider trait. The backtrace feature, when enabled, allows error types to capture and store a std::backtrace::Backtrace at the point where the error is created, providing debugging context that traverses the entire error chain.
Enabling Backtrace Support
// Cargo.toml
// thiserror = { version = "1.0", features = ["backtrace"] }
// Backtrace support requires Rust 1.65+ and the backtrace featureThe backtrace feature in thiserror enables automatic stack trace capture.
Basic Backtrace Capture
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Configuration error")]
pub struct ConfigError {
#[backtrace]
source: std::io::Error,
}
// The #[backtrace] attribute on the source field:
// 1. Captures a backtrace when the error is created
// 2. Makes the backtrace available through Provider API
// 3. Propagates backtraces from source errorsThe #[backtrace] attribute on source fields enables automatic backtrace handling.
Backtrace with Source Errors
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Database connection failed")]
pub struct DatabaseError {
#[source]
#[backtrace]
source: sqlx::Error,
}
// Both #[source] and #[backtrace] together:
// - source: Indicates the underlying error
// - backtrace: Captures backtrace and propagates from source
fn connect_to_database() -> Result<(), DatabaseError> {
// When DatabaseError is created:
// 1. Captures current backtrace
// 2. Stores reference to sqlx::Error
// 3. Backtrace available via Provider API
// If sqlx::Error also has a backtrace, it propagates up
Err(DatabaseError {
source: sqlx::Error::new(...)
})
}The combination of #[source] and #[backtrace] enables backtrace propagation through error chains.
Multiple Source Fields
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("IO error: {0}")]
Io(#[source] #[backtrace] std::io::Error),
#[error("Parse error")]
Parse {
#[source]
#[backtrace]
source: std::num::ParseIntError,
},
#[error("Custom error")]
Custom {
message: String,
#[source]
#[backtrace]
source: anyhow::Error,
},
}
// Each variant can have its own source with backtraceEach error variant can independently capture backtraces.
Manual Backtrace Field
use thiserror::Error;
use std::backtrace::Backtrace;
#[derive(Error, Debug)]
#[error("Processing failed")]
pub struct ProcessingError {
message: String,
#[backtrace]
backtrace: Backtrace,
}
// When created, ProcessingError captures a backtrace
// stored in the backtrace field
fn process() -> Result<(), ProcessingError> {
// Backtrace captured at creation point
Err(ProcessingError {
message: String::from("failed"),
backtrace: Backtrace::capture(),
})
}A dedicated backtrace field captures the stack trace at error creation.
Backtrace in Tuple Structs
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Network error")]
pub struct NetworkError(#[source] #[backtrace] std::io::Error);
// Tuple struct with backtrace on source field
#[derive(Error, Debug)]
#[error("Multiple errors")]
pub struct MultiError(
String,
#[source] #[backtrace] std::io::Error,
);
// Multiple fields with backtraceTuple structs support #[backtrace] on source fields.
Retrieving Backtraces
use thiserror::Error;
use std::backtrace::Backtrace;
use std::error::Error;
use std::any::Provider;
#[derive(Error, Debug)]
#[error("Operation failed")]
pub struct OperationError {
#[backtrace]
backtrace: Backtrace,
}
fn retrieve_backtrace() {
let error = OperationError {
backtrace: Backtrace::capture(),
};
// Access backtrace through the error reference
// In Rust 1.65+, backtraces are accessible via:
// - The Display trait (prints backtrace)
// - Provider API for programmatic access
println!("Error: {}", error);
// Backtrace included in output based on RUST_BACKTRACE setting
}Backtraces are retrieved through the error's display or Provider API.
Backtrace Propagation Chain
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Low-level error")]
pub struct LowLevelError {
#[backtrace]
source: std::io::Error,
}
#[derive(Error, Debug)]
#[error("Mid-level error")]
pub struct MidLevelError {
#[source]
#[backtrace]
source: LowLevelError,
}
#[derive(Error, Debug)]
#[error("High-level error")]
pub struct HighLevelError {
#[source]
#[backtrace]
source: MidLevelError,
}
// Backtrace chain:
// HighLevelError -> MidLevelError -> LowLevelError -> io::Error
//
// Each level can capture its own backtrace
// The full chain is available when displayedBacktraces propagate through error chains when all levels use #[backtrace].
RUST_BACKTRACE Environment Variable
use thiserror::Error;
use std::backtrace::Backtrace;
#[derive(Error, Debug)]
#[error("Failed to process")]
pub struct ProcessError {
#[backtrace]
backtrace: Backtrace,
}
// Backtrace capture is controlled by RUST_BACKTRACE:
// RUST_BACKTRACE=0 - Backtrace::capture() returns Backtrace::disabled()
// RUST_BACKTRACE=1 - Capture backtrace, limit depth
// RUST_BACKTRACE=full - Capture complete backtrace
// In code:
fn demonstrate_backtrace_settings() {
// The Backtrace::capture() behavior depends on env var:
let backtrace = Backtrace::capture();
// If RUST_BACKTRACE is not set, backtrace will be empty
// If set, captures the current call stack
}Environment variables control backtrace capture behavior.
Conditional Backtrace Feature
// Cargo.toml
// [dependencies]
// thiserror = { version = "1.0", optional = true }
//
// [features]
// default = []
// backtrace = ["thiserror/backtrace"]
use thiserror::Error;
#[derive(Error, Debug)]
#[cfg(feature = "backtrace")]
#[error("Operation error with backtrace")]
pub struct OperationError {
#[backtrace]
backtrace: std::backtrace::Backtrace,
}
#[derive(Error, Debug)]
#[cfg(not(feature = "backtrace"))]
#[error("Operation error")]
pub struct OperationError;
// The backtrace feature can be optional
// Compile with/without backtrace supportMake backtrace capture an optional feature for conditional compilation.
Backtrace Display Format
use thiserror::Error;
use std::backtrace::Backtrace;
#[derive(Error, Debug)]
#[error("Failed to connect")]
pub struct ConnectionError {
address: String,
#[source]
#[backtrace]
source: std::io::Error,
}
// When displayed, includes:
// - Error message: "Failed to connect"
// - Source chain: io::Error details
// - Backtrace: Stack trace at error creation
//
// Example output:
// Failed to connect
// Caused by:
// Connection refused
//
// Stack backtrace:
// 0: std::backtrace::Backtrace::capture
// 1: connect_to_server
// 2: main
// ...Error display includes backtrace when captured and environment permits.
Comparison: With and Without Backtrace
use thiserror::Error;
// Without backtrace
#[derive(Error, Debug)]
#[error("Simple error")]
pub struct SimpleError {
#[source]
source: std::io::Error,
}
// No backtrace capture, smaller error type
// With backtrace
#[derive(Error, Debug)]
#[error("Error with context")]
pub struct DetailedError {
#[source]
#[backtrace]
source: std::io::Error,
}
// Backtrace captured, more debugging context
// Memory comparison:
// - SimpleError: Just the source error
// - DetailedError: Source + Backtrace (~several KB)Backtraces add memory overhead but provide debugging context.
Backtrace in Anyhow Integration
use thiserror::Error;
use anyhow::Context;
#[derive(Error, Debug)]
#[error("Application error")]
pub struct AppError {
#[source]
#[backtrace]
source: anyhow::Error,
}
fn with_anyhow_backtrace() -> Result<(), AppError> {
// anyhow captures backtraces automatically when RUST_BACKTRACE=1
std::fs::read_to_string("config.txt")
.context("Failed to read config")?; // anyhow adds backtrace
Ok(())
}
// thiserror can propagate anyhow's backtrace through #[backtrace]thiserror integrates with anyhow for backtrace propagation.
Struct Size Considerations
use thiserror::Error;
use std::backtrace::Backtrace;
use std::mem::size_of;
#[derive(Error, Debug)]
#[error("Error without backtrace")]
pub struct SmallError {
#[source]
source: std::io::Error,
}
#[derive(Error, Debug)]
#[error("Error with backtrace")]
pub struct LargeError {
#[source]
#[backtrace]
source: std::io::Error,
}
fn compare_sizes() {
// Backtrace adds significant size
// SmallError: ~size of io::Error
// LargeError: SmallError + Backtrace overhead
println!("io::Error: {} bytes", size_of::<std::io::Error>());
println!("Backtrace: {} bytes", size_of::<Backtrace>());
// Backtrace is typically several hundred bytes to several KB
}Backtraces increase error type size significantly.
Practical Usage Pattern
use thiserror::Error;
use std::backtrace::Backtrace;
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Connection failed")]
Connection {
#[source]
#[backtrace]
source: std::io::Error,
},
#[error("Query failed")]
Query {
query: String,
#[source]
#[backtrace]
source: sqlx::Error,
},
#[error("Timeout after {ms}ms")]
Timeout {
ms: u64,
#[backtrace]
backtrace: Backtrace,
},
#[error("Invalid configuration")]
Config {
field: String,
reason: String,
#[backtrace]
backtrace: Backtrace,
},
}
// Practical pattern:
// - External errors (io, network): #[source] #[backtrace]
// - Internal errors: #[backtrace] field
// - Each captures context at failure pointA typical pattern uses #[backtrace] for both source errors and dedicated backtrace fields.
Synthesis
Quick comparison:
| Attribute | Purpose | Effect |
|---|---|---|
#[source] |
Mark underlying error | Enables error chain |
#[backtrace] |
Enable backtrace capture | Captures stack trace |
#[source] #[backtrace] |
Propagate + capture | Both chain and trace |
Usage patterns:
use thiserror::Error;
use std::backtrace::Backtrace;
// Pattern 1: Propagate source backtrace
#[derive(Error, Debug)]
#[error("Wrapper error")]
pub struct WrapperError {
#[source]
#[backtrace]
source: std::io::Error,
}
// Pattern 2: Capture own backtrace
#[derive(Error, Debug)]
#[error("Own error")]
pub struct OwnError {
#[backtrace]
backtrace: Backtrace,
}
// Pattern 3: Both source and own backtrace
#[derive(Error, Debug)]
#[error("Detailed error")]
pub struct DetailedError {
#[source]
#[backtrace]
source: std::io::Error,
#[backtrace]
backtrace: Backtrace,
}Key insight: thiserror's #[backtrace] attribute automates the boilerplate of capturing stack traces and propagating them through error chains. Without this feature, you'd need to manually implement the std::any::Provider trait and handle backtrace storage yourself. The #[backtrace] attribute on a source field ensures that when an error wraps another error, the backtrace is properly propagated up the chain, allowing developers to see the complete call stack from where the original error occurred to where it was handled. Combined with RUST_BACKTRACE=1 or RUST_BACKTRACE=full, this provides comprehensive debugging context without requiring manual implementation. The feature is opt-in (requires both the Cargo feature flag and the attribute) because backtraces have runtime overhead both in memory (storing the trace) and time (capturing it), so it's appropriate to enable only when debugging context is needed.
