Skip to content
rusthacks.dev
Tooling

Reading Rust Backtraces and Setting the Right Variables

A panic prints a message and one line of context. Set RUST_BACKTRACE to see the stack, find your frames in the noise and know what debug symbols change.

3 min read
Summarize in AI
Flat illustration for the post “Reading Rust Backtraces and Setting the Right Variables”, composed around tooling.

The panic message tells you what happened, and the location points at the line that panicked, which is often not the line that caused it. A backtrace shows the path that led there. Rust does not print one by default, and when you do turn it on, the output has twenty frames of runtime plumbing before the one you need. This post shows how to read it. It is the start of the debugging cluster in the tooling topic.

Rust 1.90.

Turn it on

rust
fn parse(s: &str) -> i32 {
    s.parse().unwrap()
}

fn total(items: &[&str]) -> i32 {
    items.iter().map(|s| parse(s)).sum()
}

fn main() {
    println!("{}", total(&["1", "2", "x"]));
}
Shell
RUST_BACKTRACE=1 cargo run
Text
thread 'main' panicked at src/main.rs:2:19:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
stack backtrace:
   0: rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::result::unwrap_failed
   3: demo::parse
             at ./src/main.rs:2:19
   4: demo::total::{{closure}}
             at ./src/main.rs:6:30
   ...
  10: demo::total
             at ./src/main.rs:6:5
  11: demo::main
             at ./src/main.rs:10:20

The frames numbered 0 to 2 are the panic machinery. Frame 3 is the first one in demo, so parse at line 2 is where it happened. Frames 4 to 11 show who called it: main called total, which called parse from a closure. The Err value tells you the input was x, which the backtrace alone would not.

Which variable

ValueEffect
unset or 0no backtrace, a hint to set the variable
1short backtrace, std internals trimmed
fullevery frame

RUST_BACKTRACE controls panics and, by default, backtraces captured by libraries. RUST_LIB_BACKTRACE overrides it for the latter only, so you can silence library captures while keeping panic traces.

Release builds

A release build keeps function names but may omit file and line information, and inlining can merge frames. If the backtrace shows only addresses or <unknown>, add debug info to the profile:

Cargo.tomlTOML
[profile.release]
debug = "line-tables-only"

This adds line tables without full debug data, a moderate increase in binary size. It does not change optimization. Stripping the binary (strip = true) removes them again.

Reading tips

  • Search for your crate name in the frames, and read from there downward.
  • Closures appear as {{closure}}, and async functions as {{closure}} inside a state machine; the async case is trickier and covered with Tokio's task dumps in the debugging posts.
  • Inlined functions may be missing; run in debug mode for the full picture.
  • A panic inside a thread names the thread, so two traces from one run are different threads.

Backtraces for errors

anyhow::Error captures a backtrace at creation when the variable is set, and prints it with {:?}. Standard Error types do not. If a Result error reaches main without context, it has no trace by default. See anyhow versus thiserror.

Limits

A backtrace shows the call stack at one moment, not the data. For values use [dbg!](/library/rust-dbg-macro) or a debugger. The panic internals are described in the standard library docs for std::backtrace.

Also inRust
Taggedbacktrace
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.