Loading page…
Rust walkthroughs
Loading page…
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.