# thistrace
`thistrace` adds **callsite provenance** (file/line/column) to `thiserror` enums **without** requiring `map_err(...)` at each callsite.
It works by generating `#[track_caller]` `From<T>` impls for `#[from]` conversions, so `?` captures the location where the conversion happened.
## What it solves
With plain `thiserror`, you often end up with an error chain but not *where your code wrapped/bubbled* the error.
`thistrace` captures a small “provenance frame” at each `#[from]` conversion boundary.
## Usage
Add dependencies:
```toml
[dependencies]
thistrace = "0.1"
thiserror = "2"
```
In larger codebases, you can keep `thistrace` usage mostly confined to your error module via:
```rust
use thistrace::prelude::*;
```
Annotate your `thiserror` enum with `#[traceable]` (or `#[thistrace::traceable]`):
```rust
use std::io;
use thistrace::prelude::*;
#[traceable]
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("io")]
Io(#[from] io::Error),
}
fn inner() -> Result<(), io::Error> {
Err(io::Error::new(io::ErrorKind::Other, "boom"))
}
fn outer() -> Result<(), AppError> {
inner()?; // provenance captured here
Ok(())
}
```
Print with provenance:
```rust
let err = outer().unwrap_err();
eprintln!("{}", thistrace::DisplayTrace::new(&err));
```
### Capturing the initiation (origin) location
By default, `#[traceable]` captures the location where a `#[from]` conversion happened (the `?` site).
If you also want the location where an error was *initiated*, wrap it with `thistrace::origin(...)`
and use `thistrace::Origin<_>` as the `#[from]` source type:
```rust
use std::io;
use thistrace::{origin, traceable};
#[traceable]
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("io")]
Io(#[from] thistrace::Origin<io::Error>),
}
fn inner() -> Result<(), thistrace::Origin<io::Error>> {
Err(origin(io::Error::new(io::ErrorKind::Other, "boom")))
}
fn outer() -> Result<(), AppError> {
inner()?; // bubble frame
Ok(())
}
```
The resulting trace includes **both** the origin frame and the bubble frame.
### Logging: `Display` vs `Debug`
- `Display` (`{}` / `%err`) prints the `thiserror` message only.
- `Debug` (`{:?}` / `?err`) will include the raw `trace` field for rewritten `#[from]` variants
(because `#[traceable]` turns them into struct-like variants `{ source, trace }`), but it is
not formatted as a pretty trace.
- `thistrace::DisplayTrace` is the recommended way to include a readable `at file:line:col` trace
in logs.
- `thistrace::OneLineTrace` provides a single-line string that includes the trace and cause chain
(useful when your log system treats newlines poorly).
### Logging: structured frames (Splunk-friendly)
If you log to Splunk/ELK/etc, prefer **structured fields** (often via `tracing` + JSON output)
and log the frames as a field:
```rust
use thistrace::{HasTrace, trace_frames};
tracing::error!(
error = %err,
trace = ?trace_frames(&err),
"request failed"
);
```
### Logging: common `tracing` patterns
One-line error string (good default):
```rust
tracing::error!(
error = %thistrace::OneLineTrace::new(&err),
"request failed"
);
```
Human-readable multi-line trace (nice in dev logs):
```rust
tracing::error!(
error = %thistrace::DisplayTrace::new(&err),
"request failed"
);
```
Example output:
```text
io
at src/config.rs:42:13
at src/main.rs:18:5
caused by: No such file or directory (os error 2)
```
Both structured frames and one-line text:
```rust
tracing::error!(
error = %err,
trace = ?thistrace::trace_frames(&err),
error_trace = %thistrace::OneLineTrace::new(&err),
"request failed"
);
```
### Capturing a bubble frame without `core::ops::function` noise
If you want to append a “bubble” frame using `map_err`, prefer a closure (or the helper macro)
instead of passing `thistrace::bubble` directly. This ensures the captured location is *your*
callsite:
```rust
// Good: captures this line in your code
result.map_err(thistrace::bubble_err!())?;
// Also good (equivalent)
result.map_err(|e| thistrace::bubble(e))?;
// Avoid: can capture inside core::ops::function.rs
// result.map_err(thistrace::bubble)?; // or bubble_any
```
If you want to bubble *and* convert back into your own error type in one step, use:
```rust
result.map_err(thistrace::bubble_into!(MyError))?;
```
If you're already returning `Result<_, thistrace::Bubbled<MyError>>`, use `rebubble_err!()` to
append another frame without producing `Bubbled<Bubbled<_>>`:
```rust
result.map_err(thistrace::rebubble_err!())?;
```
### Filling non-`Default` context fields (callsite helper)
If your error variant has extra fields that you want to fill at the callsite (instead of using
`Default::default()`), use `from_with_trace!`:
```rust
from_with_trace!(
some_fallible_call(),
MyError::Io { path: my_path.clone() }
)?;
```
To set a field to `Default::default()` explicitly, use `_`:
```rust
from_with_trace!(
some_fallible_call(),
MyError::Io { path: _ }
)?;
```
## Limitations
- **Only captures at conversion boundaries**: If you `?` the *same* error type upward (no `From` conversion), there is no stable way to auto-capture without an explicit wrapper step.
- **Limited `#[from]` shapes**: supports tuple variants with exactly one `#[from]` field (e.g. `Io(#[from] io::Error)`), tuple variants with extra context fields (e.g. `Io(#[from] io::Error, PathBuf)`), and struct variants with a single `#[from]` field (e.g. `Io { #[from] source: io::Error, path: PathBuf }`). Extra fields are initialized with `Default::default()`.
- **Generics**: supported for `#[traceable]` enums, including `where` clauses.
- **Non-`Default` context fields**: use `from_with_trace!(...)` to fill fields at the callsite while still capturing a trace frame. You can also use `_` to set a field to `Default::default()`.
## License
Licensed under either of
- Apache License, Version 2.0 (`LICENSE-APACHE`)
- MIT license (`LICENSE-MIT`)
at your option.