Advanced Usage

March 23, 2026 · View on GitHub

Most users only need At<E>, at!(), .at(), and .map_err_at() — see the README. This document covers everything else: embedding traces in your own error types, tuning allocation behavior, workspace layouts, output formatting, and link customization.

Internal Data Structure

graph LR
    subgraph AtE["At&lt;E&gt; (sizeof E + 8 bytes)"]
        direction TB
        error["error: E (inline)"]
        trace_ptr["trace: Option&lt;Box&lt;AtTrace&gt;&gt;"]
    end

    trace_ptr -->|"None on Ok path"| AtTraceBox

    subgraph AtTraceBox["AtTrace (112 bytes default)"]
        direction TB
        locs["locations: InlineVec&lt;Option&lt;&amp;Location&gt;, 4&gt;"]
        crate_info["crate_info: Option&lt;&amp;'static AtCrateInfo&gt;"]
        ctxs["contexts: Option&lt;Box&lt;Vec&lt;(u16, AtContext)&gt;&gt;&gt;"]
    end

Locations are stored oldest-first in an inline vector (4 slots before heap spill). Each location is an Option<&'static Location>None marks a skipped-frames placeholder ([...]).

Contexts are stored separately in a Vec<(u16, AtContext)> where the u16 is the index into the locations array. This means:

  • Adding a location (.at()) is just a push — no context allocation needed
  • Adding context (.at_str()) lazily allocates the context vec on first use
  • Multiple contexts can point to the same location index
  • The context vec is Option<Box<Vec<...>>> — 8 bytes when empty, no allocation until first context
locations:  [ loc0,   loc1,   loc2,   None  ]
               │        │       │       └─ skipped frames marker
               │        │       └─ src/handler.rs:42:5
               │        └─ src/db.rs:89:9
               └─ src/db.rs:15:13 (origin)

contexts:   [ (2, Text("processing request")),
              (2, Data(request_id: 7)),
              (1, Text("user lookup failed")) ]
              │    └─ points to locations[2]
              └─ index into locations array

This design keeps the common case (locations only, no context) allocation-free beyond the initial Box<AtTrace>.

Embedded Traces (AtTraceable)

graph TD
    Q["Do you control the error type?"]
    Q -->|No| W["Use At&lt;E&gt; wrapper"]
    Q -->|Yes| Q2["Want callers to see your type directly?"]
    Q2 -->|"Yes — Result&lt;T, MyError&gt;"| T["Implement AtTraceable"]
    Q2 -->|"No — At&lt;E&gt; is fine"| W

Most crates should use At<E>. Implement AtTraceable only when you need callers to see Result<T, MyError> directly — e.g., a public library API where At<> would be a leaky abstraction.

To embed the trace, store an AtTrace field in your error type:

use whereat::{AtTrace, AtTraceable, ResultAtTraceableExt};

struct MyError {
    kind: ErrorKind,
    trace: AtTrace,
}

impl AtTraceable for MyError {
    fn trace_mut(&mut self) -> &mut AtTrace { &mut self.trace }
    fn trace(&self) -> Option<&AtTrace> { Some(&self.trace) }
    fn fmt_message(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self.kind)
    }
}

impl MyError {
    #[track_caller]
    fn new(kind: ErrorKind) -> Self {
        Self { kind, trace: AtTrace::capture() }
    }
}

// Now use ResultAtTraceableExt instead of ResultAtExt
fn caller() -> Result<(), MyError> {
    inner().at_str("context")?;
    Ok(())
}

Storage Options

Choose trace storage based on your error type's size constraints:

Field TypeSizeBehavior
AtTrace40 bytesTrace always captured at construction
Box<AtTrace>8 bytesSmaller error, trace always heap-allocated
Option<Box<AtTrace>>8 bytesLazy allocation on first .at_*() call

For lazy allocation, implement trace_mut with lazy init:

struct MyError {
    kind: ErrorKind,
    trace: Option<Box<AtTrace>>,
}

impl AtTraceable for MyError {
    fn trace_mut(&mut self) -> &mut AtTrace {
        self.trace.get_or_insert_with(|| Box::new(AtTrace::new()))
    }
    fn trace(&self) -> Option<&AtTrace> { self.trace.as_deref() }
    // ...
}

Converting Between At and AtTraceable

// At<A> → At<B>: map_error() preserves trace
let b: At<KindB> = a.map_error(|kind| convert(kind));

// At<A> → CustomError: into_traceable() transfers trace
let custom: CustomError = at_err.into_traceable(|kind| CustomError::from(kind));

// CustomError → At<B>: into_at() transfers trace
let at_b: At<KindB> = custom.into_at(|e| convert(e.kind));

Complex Workspace Layouts

When Workspace Root != Git Root

If your crate lives in a subdirectory of a larger repository:

my-monorepo/           ← git root
├── .git/
├── services/
│   └── api/
│       └── crates/
│           └── mylib/  ← your crate here
│               ├── Cargo.toml
│               └── src/

Configure the path from git root to your crate:

whereat::define_at_crate_info!(
    path = "services/api/crates/mylib/",
);

Runtime Path Detection

For dynamic environments (monorepos with varying layouts), compute the path at init time:

use std::sync::OnceLock;
use whereat::AtCrateInfo;

static CRATE_INFO: OnceLock<AtCrateInfo> = OnceLock::new();

pub(crate) fn at_crate_info() -> &'static AtCrateInfo {
    CRATE_INFO.get_or_init(|| {
        // Compute path based on environment
        let path = std::env::var("CRATE_PATH_IN_REPO")
            .unwrap_or_else(|_| "crates/mylib/".into());

        AtCrateInfo::builder()
            .name(env!("CARGO_PKG_NAME"))
            .repo(option_env!("CARGO_PKG_REPOSITORY"))
            .commit(option_env!("GIT_COMMIT"))
            .path_owned(Some(path))
            .build()
    })
}

The _owned() builder methods leak strings via Box::leak for 'static lifetime.

build() auto-detects the link format from CARGO_PKG_REPOSITORY — no configuration needed for common forges:

ForgeAuto-detected from URLFormat Constant
GitHubgithub.comGITHUB_LINK_FORMAT
GitLabgitlab.com, gitlab.*GITLAB_LINK_FORMAT
Gitea/Forgejogitea.*, forgejo.*, codeberg.orgGITEA_LINK_FORMAT
Bitbucketbitbucket.org, bitbucket.*BITBUCKET_LINK_FORMAT

define_at_crate_info!() uses this automatically. No action needed unless you're on a self-hosted forge with a non-standard domain.

Manual Override

use whereat::{AtCrateInfo, GITLAB_LINK_FORMAT};

// Explicit .link_format() overrides auto-detection
static INFO: AtCrateInfo = AtCrateInfo::builder()
    .name("mylib")
    .repo(Some("https://my-gitlab.internal/org/repo"))
    .link_format(GITLAB_LINK_FORMAT)  // self-hosted, domain doesn't match
    .build();

Custom Format

// Placeholders: {repo}, {commit}, {path}, {file}, {line}
const MY_FORMAT: &str = "{repo}/browse/{path}{file}?at={commit}#L{line}";

static INFO: AtCrateInfo = AtCrateInfo::builder()
    .link_format(MY_FORMAT)
    .build();

Allocation Behavior

Default (Heap)

By default, traces use 4 inline location slots plus heap overflow. Each .at() call may allocate if the inline capacity is exceeded.

Inline Storage Features

For performance-critical code, enable inline storage to reduce allocations:

FeatureInline Slotssizeof(AtTrace)Best For
_tinyvec-64-bytes4≤64 bytesVery shallow traces
_tinyvec-128-bytes12≤128 bytesTypical traces
_tinyvec-256-bytes28≤256 bytesDeep traces
_tinyvec-512-bytes60≤512 bytesVery deep traces
_smallvec-128-bytes12≤128 bytesBest Linux perf
_smallvec-256-bytes28≤256 bytesBest Windows perf for deep traces
[dependencies]
whereat = { version = "0.1", features = ["_tinyvec-128-bytes"] }

Recommendations:

  • Linux: _smallvec-128-bytes for all frame counts
  • Windows: _smallvec-128-bytes for ≤12 frames, _smallvec-256-bytes for >12
  • Cross-platform default: _tinyvec-128-bytes

OOM Handling

  • Vec and String operations use try_reserve — silently skip on OOM
  • Box allocations use Box::new — can panic (waiting for Box::try_new stabilization)
  • The error E is always stored inline in At<E>, so errors propagate even if tracing fails

Pretty Output Formatters

FormatterShowsRequires
format!("{:?}", err)Debug — full trace with all contexts
format!("{}", err)Display — just the error message
err.full_trace()Message + locations + all contexts
err.last_error_trace()Message + locations (no contexts)
err.display_with_meta()Full trace with repository links
err.display_color()Colored terminal output_termcolor feature
err.display_color_meta()Colored output with repo links_termcolor feature
err.display_html()HTML (no styles)_html feature
err.display_html_styled()HTML with embedded CSS_html feature

The built-in formatters (no feature required) work everywhere. The optional formatters need feature flags:

Terminal Colors (_termcolor feature)

[dependencies]
whereat = { version = "0.1", features = ["_termcolor"] }
use whereat::{at, At};

#[derive(Debug)]
struct MyError;

let err: At<MyError> = at(MyError).at_str("loading config");

// Colored output (uses owo-colors)
println!("{}", err.display_color());

// Colored output with GitHub/GitLab links
println!("{}", err.display_color_meta());

Output uses ANSI colors:

  • Error type in red
  • File paths in cyan
  • Line numbers in yellow
  • Context strings in dimmed

HTML Output (_html feature)

[dependencies]
whereat = { version = "0.1", features = ["_html"] }
// Basic HTML (no styles, use your own CSS)
println!("{}", err.display_html());

// HTML with embedded <style> block
println!("{}", err.display_html_styled());

Example styled HTML output:

<div class="whereat-error">
<div class="error-header">Error: MyError</div>
<div class="location"><span class="at-prefix">at </span><span class="file">src/main.rs</span><span class="at-prefix">:</span><span class="line">42</span></div>
<div class="context">╰─ <span class="context-text">loading config</span></div>
</div>

Running the Example

cargo run --example pretty_output --features "_termcolor,_html"

Benchmarks

See docs/BENCHMARK.md for detailed performance comparisons.

Quick summary:

  • whereat is 150x faster than backtrace crate at same frame depth
  • whereat is 25-40x faster than panic+catch_unwind
  • At<E> wrapper has zero overhead when no frames are captured
  • Per-frame cost: ~16ns (Copy types), ~25ns (heap types)