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)
plain enum ████ 27ns
thiserror ████ 27ns
anyhow █████ 34ns ← no location info
whereat (1 frame) ██████ 40ns ← file:line:col captured
whereat (3 frames) ███████ 46ns
With RUST_BACKTRACE=1:
anyhow ██████████████████████████████████ 2,500ns (63x slower)
backtrace crate █████████████████████████████████████████████████████ 6,300ns
panic + catch_unwind █████████████████ 1,300ns
Fair comparison (same 10-frame depth, 10k errors):
whereat .at() ██ 656µs ← 150x faster than backtrace
panic + catch_unwind ██████████████████████ 22ms
backtrace crate ██████████████████████████████████████████████████ 99ms
anyhow/panic only capture backtraces when RUST_BACKTRACE=1. whereat always captures location.
Linux x86_64. See cargo bench --bench nested_loops "fair_10fr" for full results.
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