2.4 · Backtraces: Capturing Stack Traces
Domain 2 — Error Handling Ecosystem Duration: ~15 minutes Library components:
std::backtrace::Backtrace,std::backtrace::BacktraceStatus
Introduction
A backtrace shows the call stack at the moment that the program created an error. It shows which function called which function, back to main. Without a backtrace, the diagnosis of a production failure in complex async code or library code can take hours. With a backtrace, the call site of the failure is immediately visible.
std::backtrace::Backtrace (stable since Rust 1.65) captures this information when you request it. With the Error::provide mechanism (nightly: error_generic_member_access), you can embed a backtrace in a custom error type. Then each consumer can get the backtrace and does not need to know the concrete type.
This tutorial shows:
Backtrace::captureand theRUST_BACKTRACEenvironment variable.- The
BacktraceStatusvariants:Captured,Disabled,Unsupported. Backtrace::force_capture, and when to use it in place ofcapture.- The performance cost of a capture that is always on, compared with a conditional capture.
- How to embed a
Backtracein a customErrortype throughError::provide.
Backtrace::capture and BacktraceStatus
Backtrace::capture() always returns a Backtrace object. Its contents depend on the RUST_BACKTRACE environment variable:
RUST_BACKTRACE value | BacktraceStatus | Frames available? |
|---|---|---|
not set or 0 | Disabled | No |
1 | Captured | Yes |
full | Captured | Yes (the same frames as 1) |
| The platform has no backtrace support | Unsupported | No |
Figure: Backtrace Capture Decision Flow
use std::backtrace::{Backtrace, BacktraceStatus};
// capture() obeys RUST_BACKTRACE. It always returns a Backtrace object.
let bt = Backtrace::capture();
match bt.status() {
BacktraceStatus::Captured => println!("{bt}"), // Display prints the frames
BacktraceStatus::Disabled => println!("run with RUST_BACKTRACE=1"),
BacktraceStatus::Unsupported => println!("not supported here"),
_ => {} // BacktraceStatus is #[non_exhaustive], so the match needs this arm
}
The Display implementation of Backtrace prints the stack trace as text that you can read. {bt} prints the short form. The alternate form {bt:#} prints the full form, which adds the instruction address of each frame.
The value of RUST_BACKTRACE (1 or full) does not change this output. It changes only the backtrace that the default panic hook prints. With 1, that hook omits some frames. With full, it prints all frames (standard library internals included).
When to check status before logging
// `log::error!` is the macro of the external `log` crate. It is here as an example logger.
let bt = Backtrace::capture();
if bt.status() == BacktraceStatus::Captured {
log::error!("backtrace:\n{bt}"); // only this branch formats the frames
} else {
log::error!("(enable RUST_BACKTRACE=1 for backtrace)");
}
It is cheap to create the Backtrace each time and to check its status, because a status check only reads a flag. The format operation with {} does the work.
With RUST_BACKTRACE not set, 02_15_backtrace_capture.rs prints:
Backtrace status: Disabled (set RUST_BACKTRACE=1 to enable)
Backtrace::capture() returned a backtrace object.
To see frames, re-run with RUST_BACKTRACE=1
Backtrace not available; logging error message only.
All assertions passed.
(With RUST_BACKTRACE=1, the status is Captured, and the example prints the start of the trace.)
Backtrace::force_capture
force_capture() always captures a full backtrace and ignores RUST_BACKTRACE. The status of the result is always BacktraceStatus::Captured (when the platform supports backtraces).
let normal = Backtrace::capture(); // obeys RUST_BACKTRACE
let forced = Backtrace::force_capture(); // always captures
// True for each value of RUST_BACKTRACE, on a platform that supports backtraces.
assert_eq!(forced.status(), BacktraceStatus::Captured);
capture() vs force_capture(): when to use each
| Scenario | Use |
|---|---|
| Production error types | capture(): cheap when disabled, available when necessary |
| Test assertion helpers | force_capture(): you always want the call site |
| Diagnostic tools (always on) | force_capture() |
| Hot paths where performance is important | capture() with RUST_BACKTRACE=0 |
Performance cost
force_capture() walks the native stack on each call. When backtraces are disabled, capture() does not walk the stack, so its cost is almost zero. The difference is typically 100x–1000x. With RUST_BACKTRACE not set, 02_16_force_capture.rs prints:
capture() status: Disabled
force_capture() status: Captured
capture() x1000: 16.791µs # (varies)
force_capture() x1000: 9.044083ms # (varies)
With RUST_BACKTRACE unset, capture() is ~538x faster than force_capture(). # (varies)
force_capture Display (first 200 chars):
0: std::backtrace_rs::backtrace::libunwind::trace # (varies)
at /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9 # (varies)
1: std::backtra # (varies)
All assertions passed.
The times and the ratio vary with the machine and the stack depth. The frame lines vary with the platform and the toolchain.
For production error types that a program may construct millions of times per second, use capture(). Use force_capture() only for code paths that always need the trace, whatever the environment of the operator is.
Embedding a Backtrace in a Custom Error
The standard pattern has two steps. First, capture the backtrace when you construct the error. Then, make the backtrace available to consumers through Error::provide.
Note:
Error::providerequires theerror_generic_member_accessnightly feature (tracking issue #99301).
#![feature(error_generic_member_access)] // nightly only
use std::backtrace::Backtrace;
use std::error::{Error, Request};
use std::io;
#[derive(Debug)]
struct StorageError {
operation: String,
source: io::Error, // the cause: source() returns it
backtrace: Backtrace, // the context: provide() gives it to consumers
}
impl StorageError {
fn new(operation: impl Into<String>, source: io::Error) -> Self {
Self {
operation: operation.into(),
source,
backtrace: Backtrace::capture(), // capture at the construction site
}
}
}
// The Error trait also requires a Display implementation. It is not shown here.
impl Error for StorageError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
// A consumer requests a value by its type. This method answers a request for Backtrace.
fn provide<'a>(&'a self, request: &mut Request<'a>) {
request.provide_ref::<Backtrace>(&self.backtrace);
}
}
A consumer gets the backtrace and does not need to know the concrete type:
// Works for each error type. Returns None if the error provides no Backtrace.
fn get_backtrace(err: &dyn Error) -> Option<&Backtrace> {
std::error::request_ref::<Backtrace>(err)
}
// read_record(0) returns Err(StorageError), because record 0 does not exist.
let err = read_record(0).unwrap_err();
if let Some(bt) = get_backtrace(&err) {
println!("backtrace status: {:?}", bt.status()); // Disabled or Captured
println!("{bt}"); // prints "disabled backtrace" when the status is Disabled
}
// The same function works through Box<dyn Error>.
let boxed: Box<dyn Error> = Box::new(read_record(0).unwrap_err());
let bt = get_backtrace(&*boxed); // Some(&Backtrace)
Why capture at construction, not at the call site?
A program can create an error at a deep level of a call chain:
main → fetch → read_record → StorageError::new
A capture in StorageError::new gives you the frame that caused the failure. A capture at a higher level (for example, in fetch) shows only the call to fetch, and not the internal path.
The integration with the error chain
source() and provide() are complementary:
source()gives the cause: a different error.provide()gives the context: typed values that this specific error holds.
A consumer that logs all of this information can use this function:
// `log::error!` is the macro of the external `log` crate. It is here as an example logger.
fn log_error(err: &dyn Error) {
log::error!("error: {err}");
// Log the backtrace if the error provides one.
if let Some(bt) = std::error::request_ref::<Backtrace>(err) {
log::error!("backtrace:\n{bt}");
}
// Go through the chain of causes. source() returns None at the end of the chain.
let mut cause = err.source();
while let Some(c) = cause {
log::error!("caused by: {c}");
cause = c.source();
}
}
With RUST_BACKTRACE not set, 02_17_backtrace_in_errors.rs prints:
read_record(42): Ok("record-42")
error: storage operation "read" failed
source: Some("record 0 does not exist")
backtrace status: Disabled
(backtrace disabled — run with RUST_BACKTRACE=1)
backtrace through Box<dyn Error>: Some(Disabled)
io::Error (no provide): None
All assertions passed.
(With RUST_BACKTRACE=1, the backtrace status is Captured, and the example prints the first frames.)
When to Capture a Backtrace and When Not To
Backtraces help when:
- You debug unexpected errors in production (set
RUST_BACKTRACE=1in the environment). - You write tools for post-mortem analysis.
- You write test assertion helpers that must show where an assertion failed.
Backtraces hurt when:
- The program constructs errors on hot paths (thousands per second). Use
capture(), notforce_capture(). - Memory is limited. Each
Backtraceobject holds a vector of frames. - The program expects the errors and handles them silently. A disabled
Backtrace::capture()has almost zero cost, so this is a problem only forforce_capture().
The standard practice for library authors is to embed Backtrace::capture() in error types. This has no cost when the operator did not enable RUST_BACKTRACE. It gives the full context when the operator enabled it.
Summary
| API | What it does |
|---|---|
Backtrace::capture() | Captures if RUST_BACKTRACE enables backtraces. If not, returns a disabled backtrace |
Backtrace::force_capture() | Always captures, and ignores RUST_BACKTRACE |
bt.status() | Returns Captured, Disabled, or Unsupported |
format!("{bt}") / println!("{bt}") | Formats the stack trace as text |
Error::provide + request.provide_ref::<Backtrace> | Embeds a backtrace in an error type (nightly) |
std::error::request_ref::<Backtrace>(err) | Gets the backtrace from any dyn Error (nightly) |
Code Examples
| File | Description |
|---|---|
02_15_backtrace_capture.rs | Backtrace::capture, BacktraceStatus, conditional logging |
02_16_force_capture.rs | force_capture compared with capture, performance comparison |
02_17_backtrace_in_errors.rs | A Backtrace in a custom Error through Error::provide (nightly) |