R
RUSTY YELLOW PAGES

#[track_caller]Attribute


Explanation

#[track_caller] is placed on a fn item and changes what core::panic::Location::caller() reports when called anywhere inside that function's body (directly, or transitively through other #[track_caller] functions it calls): instead of reporting the source location where Location::caller() itself is written, it reports the location of the call site that invoked the #[track_caller] function — the caller's line, not the function's own definition.

This is precisely the mechanism behind one of the most-noticed pieces of polish in the standard library: calling .unwrap() on a None, or indexing a Vec out of bounds, reports the panic at your line of code — the line in your own project where you wrote .unwrap() or v[i] — rather than pointing uselessly at a line deep inside the standard library's implementation of Option::unwrap or Index for Vec. Every one of those standard library functions is itself marked #[track_caller]; without it, a panic location would always point at the one line inside core/std where the panic! macro (which reads Location::caller() internally to build its message) is actually written, which is true but useless to a caller trying to find the bug in their own code.

The attribute is transitive in a specific sense: if a #[track_caller] function f calls another #[track_caller] function g, and g reads Location::caller(), that call reports the location that called f — the location information passes through the chain of #[track_caller] calls to whoever ultimately triggered the outermost one. A regular (non-#[track_caller]) function in the middle of that chain breaks the chain: it reports its own call site to whatever it calls, not the location further up that called it.

This has no runtime cost in the way it sounds like it might — no stack unwinding or backtrace capture happens just from adding the attribute. The caller's location is passed as an implicit, extra piece of information at the call site itself (conceptually similar to an extra hidden argument), resolved entirely at compile time; it's a zero-overhead, compile-time-wired mechanism, not a runtime stack walk.

#[track_caller] cannot be combined with certain other constructs — most notably it has no effect through a dyn Trait call, since the concrete Location wiring is resolved statically per call site and a trait object call has erased which concrete call site is calling through it.

Usage examples

Reporting the caller's location from a panic

#[track_caller] // <- makes Location::caller() below report the CALLER's line, not this one
fn require_positive(value: i32) {
    if value <= 0 {
        let loc = std::panic::Location::caller();
        panic!("value must be positive, got {value} at {loc}");
    }
}

fn main() {
    require_positive(-1); // the panic message reports THIS line, not the one inside require_positive
}

Validating input

A configuration-loading helper used throughout an application validates a port number and panics on an invalid one; marking it #[track_caller] makes every panic point at the call site that passed the bad value, mirroring exactly how Option::unwrap reports the caller's line instead of its own.

#[track_caller] // <- panics from inside this function report the CALLER's source location
fn require_valid_port(port: u32) -> u16 {
    u16::try_from(port).unwrap_or_else(|_| {
        panic!("port {port} is out of range for u16") // reports the call site below, not this line
    })
}

fn main() {
    let port = require_valid_port(70_000); // <- the panic message points here
    println!("listening on {port}");
}

Without #[track_caller], every misconfigured call anywhere in a large codebase would report the same unhelpful location — the panic! line inside require_valid_port itself — instead of the call site that actually passed the bad value; the std documentation for #[track_caller] describes this as exactly the mechanism Option::unwrap/expect and slice indexing use internally, and applying it to custom validation/assertion helpers extends the same caller-friendly diagnostics to application code.

Testing

A custom test-assertion helper used across a test suite should report a failed assertion at the line inside the test that called it, not at the line inside the shared helper function every test calls into.

#[track_caller] // <- a failed assertion here reports the calling test's line, not this function's
fn assert_within_tolerance(actual: f64, expected: f64, tolerance: f64) {
    let diff = (actual - expected).abs();
    assert!(
        diff <= tolerance,
        "expected {expected} +/- {tolerance}, got {actual}"
    );
}

#[test]
fn sensor_reading_matches_calibration() {
    let reading = 21.4;
    assert_within_tolerance(reading, 21.5, 0.05); // <- failure reports THIS line
}

assert! itself is #[track_caller], so without marking assert_within_tolerance the same way, a failure would report the assert! line inside the shared helper — the same unhelpful, one-size-fits-all location for every test that uses it — instead of the specific test and assertion that actually failed; this is the standard pattern the Rust Reference describes for custom assertion helpers that want to preserve assert!-quality diagnostics through a layer of indirection.

Explanation

Embedded support: Full

#[track_caller] and core::panic::Location::caller() both live in core, with no dependency on alloc/std or an OS, so the mechanism is fully available and behaves identically under #![no_std]. It arguably matters more in embedded code than in a hosted program: a #![no_std] panic handler frequently has no attached debugger, no backtrace, and no OS-level crash report to fall back on — depending on the panic handler, all it does is blink an LED, reset the chip, or emit a message over RTT/UART. In that setting, the file/line a #[track_caller]-annotated validation helper reports may be the only diagnostic information available about where a bad value actually originated, rather than one convenience among several.

Usage examples

Reporting the caller's location from a HAL validation panic

#[track_caller] // <- panics from inside this function report the CALLER's source location
fn require_valid_channel(channel: u8) -> u8 {
    if channel >= 8 {
        panic!("ADC channel {channel} out of range (0..=7)");
    }
    channel
}

fn read_adc(channel: u8) -> u16 {
    let channel = require_valid_channel(channel); // <- an out-of-range call here is what gets reported
    // ... trigger a conversion on `channel` and return the raw sample
    0
}

Without #[track_caller], every misconfigured call across a firmware crate would report the same unhelpful line inside require_valid_channel itself — over RTT or a UART log with no debugger attached, that's often the only location information available at all.