whereat
Production error tracing without debuginfo, panic, or overhead.
After a decade of distributing server binaries, I'm finally extracting this approach into its own crate!
In production, you need to immediately know where the bug is at() — without panic!, debuginfo, or overhead. Just replace ? with .at()? in your call tree to get beautiful build-time & async-friendly stacktraces with GitHub links.
Error: UserNotFound
at src/db.rs:142:9
╰─ user_id = 42
at src/api.rs:89:5
╰─ in handle_request
at myapp @ https://github.com/you/myapp/blob/a1b2c3d/src/main.rs#L23
Compatible with plain enums, errors, structs, thiserror, anyhow, or any type with Debug. No changes to your error types required!
Performance
Error creation time (lower is better)
Ok path (no error) █ <1ns ← ZERO overhead on success
plain enum error █ <1ns
whereat (1 frame) ███ 18ns ← file:line:col captured
whereat (3 frames) ███ 19ns
whereat (10 frames) ██████████ 67ns
With RUST_BACKTRACE=1:
anyhow █████████████████████████████████████████████████ 2,500ns
backtrace crate ████████████████████████████████████████████████████████████████████████████████████████████████████ 6,300ns
panic + catch_unwind ██████████████████████████ 1,300ns
Fair comparison (same 10-frame depth, 10k iterations):
whereat .at() █ 1.2ms ← 100x faster than backtrace
panic + catch_unwind ██████████████████████ 27ms
backtrace crate ████████████████████████████████████████████████████████████████████████████████████████████████████ 119ms
anyhow/panic only capture backtraces when RUST_BACKTRACE=1. whereat always captures location.
Linux x86_64 (WSL2), 2026-01-18. See cargo bench --bench overhead and cargo bench --bench nested_loops "fair_10fr".
Quick Start
// In lib.rs or main.rs - required for at!() and at_crate!() macros
define_at_crate_info!;
use ;
For workspace crates: whereat::define_at_crate_info!(path = "crates/mylib/");
API Overview
Starting a trace:
| Function | Works on | Crate info | Use when |
|---|---|---|---|
at!(err) |
Any type | ✅ GitHub links | Default choice with define_at_crate_info!() |
at(err) |
Any type | ❌ None | Simple usage, no links needed |
err.start_at() |
Error types |
❌ None | Chaining on error values |
Extending a trace (on Result<T, At<E>>):
| Method | Effect |
|---|---|
.at() |
New frame at caller's location |
.at_str("msg") |
Add context to last frame (no new location) |
.map_err_at(|e| ...) |
Convert error type, preserve trace |
Key: .at() creates a NEW frame. .at_str() adds to the LAST frame. See Adding Context for full list.
Best Practices
DO: Keep your hot loops zero-alloc
- You do NOT need
At<>inside hot loops. Defer tracing until you exit. .at_skipped_frames()adds a[...]marker to indicate frames were skipped.
DO: Use at_crate!() at crate boundaries
- When consuming errors from other crates, this ensures backtraces show
myapp @ src/lib.rs:42instead of confusing paths.
DO: Feel free to add ergonomic aliases
type MyError = At<MyInternalError>works perfectly.
Design Philosophy
You define your own error types. whereat doesn't impose any structure on your errors — use enums, structs, or whatever suits your domain. whereat just wraps them in At<E> to add location+context+crate tracking.
Which Approach?
| Situation | Use |
|---|---|
| You have an existing struct/enum you don't want to modify | Wrap with At<YourError> |
| You want traces embedded inside your error type | Implement AtTraceable trait |
Wrapper approach (most common): Return Result<T, At<YourError>> from functions. The trace lives outside your error type.
Embedded approach: Implement AtTraceable on your error type and store an AtTrace (or Box<AtTrace>) field inside it. Return Result<T, YourError> directly. See ADVANCED.md for details.
This means you can:
- Use
thiserrorfor ergonomicDisplay/Fromimpls, oranyhow - Use any enum or struct that implements
Debug - Define type aliases like
type MyError = At<BaseError> - Access your error via
.error()or deref - Support nesting with
core::error::Error::source()
Features
- Small sizeof:
At<E>is onlysizeof(E) + 8bytes (one pointer for boxed trace) - Zero allocation on Ok path: No heap allocation until an error occurs
- Ergonomic API:
.at()on Results,.start_at()on errors,.map_err_at()for trace-preserving conversions - Context options:
.at_str(),.at_string(),.at_fn(),.at_named(),.at_data(),.at_debug(),.at_error() - Cross-crate tracing:
at!()andat_crate!()macros capture crate info for GitHub/GitLab/Gitea/Bitbucket links - Equality/Hashing:
PartialEq,Eq,Hashcompare only the error, not the trace - no_std compatible: Works with just
core+alloc
Adding Context
Add a new location frame:
result.at? // New frame with just file:line:col
result.at_fn? // New frame + captures function name
result.at_named? // New frame + custom label
Add context to the last frame (no new location):
result.at_str? // Static string (zero-cost)
result.at_string? // Dynamic string (lazy)
result.at_data? // Typed via Display (lazy)
result.at_debug? // Typed via Debug (lazy)
result.at_error? // Attach a source error
If the trace is empty, context methods create a frame first. Example:
// One frame with two contexts attached
let e = at!.at_str.at_str;
assert_eq!;
// Two frames: at!() creates first, .at() creates second
let e = at!.at.at_str;
assert_eq!;
Cross-Crate Tracing
When consuming errors from other crates, use at_crate!() to mark the boundary:
define_at_crate_info!;
The at_crate!() macro takes a Result and desugars to:
result.at_crate // Adds your crate's info as boundary marker
This ensures traces show myapp @ src/lib.rs:42 instead of confusing paths from dependencies.
Hot Loops
Don't trace inside hot loops. Defer until you exit:
Advanced Usage
See ADVANCED.md for:
- Embedded traces with
AtTraceabletrait - Custom storage options (inline vs boxed)
- Complex workspace layouts
- Link format customization (GitLab, Gitea, Bitbucket)
- Inline storage features for reduced allocations
License
MIT OR Apache-2.0