reportify 0.2.2

Typed error reports with structured diagnostic context, causes and contributing factors, configurable rendering, and panic capture.
Documentation

reportify

Errors are part of an API's contract, and deserve the same care as its types and function signatures. This crate builds typed Rust errors that can still carry rich diagnostic reports without turning every internal detail into that contract. The error stays the curated, typed boundary programs branch on. Everything else, a narrative, structured fields, a backtrace, a cause, and contributing factors, is diagnostic detail that lives in context instead, regardless of who or what ends up reading it.

use reportify::{Report, ResultExt, bail, new_whatever_type};

new_whatever_type! {
    /// Application-level error.
    pub AppError
}

fn load_config(path: &std::path::Path) -> Result<String, Report<AppError>> {
    if path.as_os_str().is_empty() {
        bail!("configuration path must not be empty");
    }

    std::fs::read_to_string(path)
        .whatever("unable to read configuration")
        .field("path", path)
}

A Whatever type such as AppError above never carries its own message. Every report needs an explicit description at the call site, instead of falling back to a generic, type-level placeholder.

return_error! is a ?-like macro for a diverging function whose own return type is already the bare error/Report, e.g., a server loop that only ever exits by failing. ResultExt::assert_ok unwraps a result, treating an error as a bug in the program rather than an external failure: it panics, showing the full rendered report, instead of propagating.

Cause and Factors

Report::escalate turns a report into a differently-typed one, keeping the original as its cause. This is the only way a cause gets set: there is no way to attach a cause to an already-existing report, only to derive a new report from an old one.

Report::with_factor/Report::with_factors attach one or more independent, contributing factors instead, e.g., several hooks that each failed. Unlike a cause, a factor makes no claim that it alone was necessary or sufficient.

Capturing Panics

A panic caught with catch_unwind becomes a Report<Panicked> with a correct backtrace and location. A bare std::panic::catch_unwind cannot recover either on its own, since by the time it returns, the stack has already unwound. Panicked keeps the raw panic payload, not just an extracted message, so callers can still distinguish a genuine bug from a deliberate, non-error use of resume_unwind, e.g., as a cancellation signal.

Call install_panic_hook near the top of main to install catch_unwind's hook eagerly, closing a narrow race where a panic on another thread at that exact moment could otherwise slip through uncaptured. install_pretty_panic_hook/ install_pretty_panic_hook_with additionally take over how an uncaught panic prints: instead of the default hook's plain banner, it renders the same way a Report does, always flagged as a bug, optionally with an issue tracker URL and extra fields like the application's own version through PrettyPanicOptions.

Logging

ResultExt::log_error/log_warning/log_info log a report through tracing at the matching level; ResultExt::ignore is log_error with the value discarded too. The rendered report becomes the event's message; the error's type name and code (when it has one) are attached as separate error.type/error.code fields, so a structured subscriber can filter or group on them without parsing the message text.

Rendering

Display/Debug render a report as a tree, causes and factors nested under arrows. Report::render takes explicit render::RenderOptions for plain ASCII instead of Unicode box-drawing, forced or disabled color, or a compact view without locations. Report::print/Report::eprint render and print directly to stdout/stderr, correcting the color mode to check whichever stream they actually print to. A verbose backtrace skips reportify's own frames and the runtime's startup frames, keeping only what the caller actually wrote. RenderOptions::wrap word-wraps long lines to a fixed width or the actual terminal width, off by default, so a long message/suggestion/field value still lines up under the tree instead of soft-wrapping wherever the terminal decides. See the render module's docs for what each combination looks like, or run cargo run --example config.

Export

Report::export/Report::export_with turn a report into structured data instead of text. Public fields are exported by default. Sensitive and secret fields, and the captured backtrace and span trace, require explicit opt-in through export::ExportOptions. The backtrace and span trace export as structured frames/spans, not rendered text, and the backtrace is the raw, unfiltered capture.

Optional features:

  • backtrace captures backtraces through the backtrace crate instead of std::backtrace, enabling frame-skipped verbose rendering, and is enabled by default.
  • color colors rendered reports through console and is enabled by default.
  • spantrace captures tracing-error span traces and is enabled by default.
  • serde derives Serialize/Deserialize for exported report data, with camelCase field names (enum variant tags stay Capitalized).