#172 May 30, 2026

172. #[track_caller] — Point the Panic at the Caller, Not Your Helper

You wrap an assert in a helper to clean up your tests. Now every failure points at the helper’s source line instead of the test that called it. #[track_caller] fixes that with a single line of code.

The problem: panics blame the helper

Say you’ve factored out a custom check used across many tests:

1
2
3
4
5
6
7
8
fn assert_positive(x: i32) {
    assert!(x > 0, "expected positive, got {x}");
}

#[test]
fn it_works() {
    assert_positive(-3); // panics
}

The panic message looks like this:

1
2
thread 'it_works' panicked at src/lib.rs:2:5:
expected positive, got -3

src/lib.rs:2 is the line inside assert_positive. Every test that uses this helper points at the same spot. Useless.

The fix: one attribute

Put #[track_caller] on the helper and the reported location becomes whichever call site invoked it:

1
2
3
4
5
6
7
8
9
#[track_caller]
fn assert_positive(x: i32) {
    assert!(x > 0, "expected positive, got {x}");
}

#[test]
fn it_works() {
    assert_positive(-3); // panics, blames THIS line
}

Now the panic points at the test’s call, exactly like a built-in assert! does. That’s because assert!, unwrap, expect, Vec::index, and friends are all themselves #[track_caller].

How it works

The attribute makes the compiler thread the caller’s Location through the function. You can grab it explicitly with core::panic::Location::caller():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::panic::Location;

#[track_caller]
fn where_am_i() -> &'static Location<'static> {
    Location::caller()
}

fn main() {
    let loc = where_am_i();
    assert_eq!(loc.line(), 11); // the call site, not the fn body
}

The attribute propagates through wrappers — mark every layer between the panic and the public API, otherwise the chain breaks at the first un-annotated function and the location resets to that frame.

When to reach for it

Any time you wrap panic!, assert!, unwrap, or expect behind a helper that callers will treat as a primitive: test assertions, domain-specific unwraps, invariant checks. The cost is zero at runtime in optimized builds — the location is baked in at compile time.

← Previous 171. assert_matches! — A Test Failure That Actually Tells You What Went Wrong