Loading pageā¦
Rust walkthroughs
Loading pageā¦
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.
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.
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.
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.
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.
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.
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 implementationThe request_ref method queries errors for attached backtraces.
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.
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.
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 propagatedthiserror generates the provide implementation automatically.
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.
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.
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.
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.
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:
std::error::Error::provideWhen 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.