traced_error 0.1.0

Typed errors with automatic trace frames and structured context for Rust error propagation
Documentation

traced_error

Typed errors with automatic trace frames and structured context for Rust error propagation.

traced_error keeps the original typed error body while recording where errors are propagated through ?. It is useful when you want debugging context similar to anyhow, but still need to match concrete error variants and inspect structured fields.

Why

Rust's ? operator propagates errors, but it does not automatically record the source location where propagation happened. Libraries such as anyhow are excellent for application-level error reports, but they erase the concrete error type behind a dynamic report.

traced_error separates these two concerns:

  • Traced<E> keeps E as the typed error body.
  • The #[traced] macro appends source locations for each rewritten ?.
  • .ctx(...) and .with_ctx(...) attach explicit displayable context payloads.

Features

  • Preserve typed error bodies instead of converting everything into a string report.
  • Automatically append source locations for ? propagation through the #[traced] macro.
  • Attach explicit structured context with .ctx(...) and .with_ctx(...).
  • Lift raw Result<T, E> values into Result<T, Traced<E>> with .ctx_lift(...) and .with_ctx_lift(...).
  • Keep traces and context across error type conversions.
  • Use SmallVec to avoid heap allocation for short trace/context stacks.

Install

[dependencies]
traced_error = "0.1"
thiserror = "1"

You normally only depend on traced_error directly. The companion proc-macro crate traced_macro is re-exported by traced_error.

Example

use std::num::ParseIntError;
use traced_error::prelude::*;

#[derive(Debug, thiserror::Error)]
enum MyError {
    #[error("parse failed: {0}")]
    Parse(#[from] ParseIntError),

    #[error("bad input")]
    BadInput,
}

#[traced]
fn parse_one(s: &str) -> Result<i64, Traced<MyError>> {
    if s.is_empty() {
        return Err(Traced::new(MyError::BadInput).ctx("input was empty"));
    }

    let n = s
        .parse()
        .with_ctx_lift(|| format!("while parsing {s:?}"))?;

    Ok(n)
}

#[traced]
fn parse_two(a: &str, b: &str) -> Result<i64, Traced<MyError>> {
    let x = parse_one(a).ctx("first operand")?;
    let y = parse_one(b).ctx("second operand")?;
    Ok(x + y)
}

fn main() {
    match parse_two("12", "not a number") {
        Ok(value) => println!("ok: {value}"),
        Err(err) => println!("{err}"),
    }
}

The error still contains MyError, so callers can continue to match on typed variants when needed.

Workspace layout

This repository contains two crates:

Crate Purpose
traced_error Public runtime API: Traced, context traits, prelude, and macro re-export.
traced_macro Proc-macro implementation for #[traced].

traced_macro does not need a separate GitHub repository. It can be published as a separate crates.io package while living in the same GitHub workspace repository.

The publish order matters because traced_error depends on traced_macro:

cargo publish -p traced_macro --dry-run
cargo publish -p traced_macro

cargo publish -p traced_error --dry-run
cargo publish -p traced_error

Repository metadata

Both crates can point to the same repository:

repository = "https://github.com/lihaohan/traced_error"
homepage = "https://github.com/lihaohan/traced_error"

There is no requirement to create https://github.com/lihaohan/traced_macro unless you want to maintain the macro crate independently.

License

Licensed under either of MIT or Apache-2.0 at your option.