Expand description
Typed error reports with structured diagnostic context, causes and contributing factors, configurable rendering, and panic capture.
§Overview
Errors are part of an API’s contract, and deserve the same care as its types and
function signatures. Propagating a single type-erased error everywhere, e.g., with
anyhow, gives callers nothing to match on: the contract at every boundary collapses
to “something failed”. Turning every internal failure into its own explicit enum
variant goes too far the other way: internal implementation detail becomes a public
commitment, so a new failure mode deep inside a function turns into a breaking change
to its signature.
This crate is built around Report<E>: a typed error, E, paired with everything
else needed to explain a failure. E is the contract, curated to include only what
callers are actually meant to handle differently, and propagated with ?. Everything
else, a message, a backtrace, structured fields, a suggestion, is diagnostic detail
that doesn’t belong in that contract, regardless of whether it ends up read by a
person, a log pipeline, or an automated triage system. The two stay separate.
§Errors and Context
A Report<E> pairs a typed error: E, the value a program branches on, with a
Context: everything else needed to explain the failure. A context holds a
narrative of Annotations (Messages describing what happened,
Suggestions for what to do about it), structured Fields, a captured backtrace,
an optional cause, and any number of contributing factors.
Sometimes there is no meaningful specific error type. Some failures will only ever be
reported, never matched on. Whatever is the escape hatch for that case: a marker
error type, defined with new_whatever_type!, that carries no data of its own and
is used through bail!, whatever!, or ResultExt::whatever. It never carries
an implicit message. Every report still needs an explicit description at the point of
failure.
§Cause and Factors
A cause and a contributing factor make different claims, so reportify keeps them
separate instead of treating both the same way.
Report::escalate produces a new, differently-typed report with self nested
inside as its Context::cause. This is the usual way a failure crosses an
abstraction boundary, and it 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, so a cause is never ambiguous about whether it actually led to the report: it
did, since escalating is what produced the report in the first place.
Report::with_factor/Report::with_factors attach one or more independent
Context::factors instead, e.g., every validation error found, not just the first
one. A factor makes no claim that it alone was necessary or sufficient, unlike a
cause. A report can have a cause, factors, or both.
§Capturing Panics
A panic caught with catch_unwind becomes a Report<Panicked>, with a real
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 tell
a genuine bug apart from a deliberate, non-error use of resume_unwind, e.g., as a
cancellation signal.
Call install_panic_hook near the top of main, before spawning any other threads,
to install catch_unwind’s hook eagerly rather than lazily on first use, closing a
narrow race where a panic on a different thread at that exact moment could otherwise
slip through uncaptured. install_pretty_panic_hook additionally takes over how an
uncaught panic prints, rendering it the same way a Report does instead of the
default hook’s plain banner, unconditionally, even for a panic some ancestor
catch_unwind goes on to recover from. Unlike a regular Report<E>, a panic is
never something the environment, configuration, or a user did wrong: it always means
a bug, so the rendered panic always says so, as a suggestion, alongside whatever
PrettyPanicOptions tells callers about reporting it, e.g., an issue tracker URL
or the application’s own version.
§Logging
ResultExt::log_error/ResultExt::log_warning/ResultExt::log_info log a
report through tracing at the matching level and return the success value as an
Option, discarding the report either way; ResultExt::ignore is log_error with
the value discarded too. The rendered report becomes the event’s message; the
error’s Error::type_name and Error::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. tracing is consequently always a
dependency, not an opt-in feature: these are the only methods that actually consume
a report, rather than annotate or propagate it further.
§Export
Report::export/Report::export_with turn a report into an
export::ExportedReport for machine consumption: structured logs, an API error
response, whatever needs the failure as data rather than text. It mirrors the report’s
own cause and factors. Fields marked Sensitive or
Secret, and the captured backtrace and span trace, are all
excluded by default and only included if the caller opts in through
export::ExportOptions. Unlike Report::render, the backtrace and span trace
stay structured data (export::ExportedFrame/export::ExportedSpan) rather than
pre-rendered text, and the backtrace is the raw, unfiltered capture: no frames are
skipped the way a rendered backtrace skips reportify’s own frames.
§Rendering
Display ({report}) and Debug ({report:?}) render a report as a tree, causes
and factors nested under arrows, the way Report::escalate and
Report::with_factor/Report::with_factors built them. Debug additionally
shows captured backtraces and span traces.
Report::render takes explicit render::RenderOptions for anything else: 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 message/suggestion/field value wider than the terminal otherwise just soft-wraps
however the terminal decides, with no indent under the part that wrapped, since there
is no newline there to attach one to. render::RenderOptions::wrap fixes that by
wrapping ahead of time to a fixed width or the actual terminal width, off by default.
A verbose backtrace skips reportify’s own frames and the runtime’s startup frames,
keeping only what the caller actually wrote, e.g., inner/middle/main rather than
also Report::new/Context::capture on one end and the runtime’s launch machinery on
the other. This needs the backtrace feature; without it, a captured backtrace
renders unfiltered.
A configuration file that could not be read at all, escalated into a higher-level error with a field and a suggestion attached, renders like this:
unable to load configuration ├╴at crates/reportify/examples/config.rs:18:10 ├╴path: config.toml ├╴suggestion: create one by copying `config.example.toml` to `config.toml` │ ╰─▶ cause: file not found ╰╴at crates/reportify/examples/config.rs:17:5
See the render module for ASCII/no-color output, a compact view, verbose
backtraces, and a report with independent factors instead of a cause.
§Getting Started
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)
}§Features
backtrace(enabled by default) captures backtraces through thebacktracecrate instead ofstd::backtrace, so a verbose render can skip reportify’s own frames and the runtime’s startup frames.color(enabled by default) colors rendered reports throughconsole, when the output looks like it is going to a terminal that supports it. Without it,render::ColorMode::Always/render::ColorMode::AutoStdout/render::ColorMode::AutoStderrbehave likerender::ColorMode::Never.spantrace(enabled by default) captures atracing-errorspan trace alongside the backtrace, so a report also shows whichtracingspans were active when it was created.serdederivesSerializeforexport::ExportedReportand the other exported types, for shipping structured logs.
Modules§
- export
- Machine-readable export of a
Report, with redaction. - render
- Configurable rendering of a
Reportas text.
Macros§
- bail
- Create a freeform report and return it as an error.
- ensure
- Return a freeform report if a condition does not hold.
- new_
whatever_ type - Define a simple
Whatevererror type. - return_
error - Unwrap a
Result, returning its error directly, for diverging functions that run forever until something fails: unlike?, which needs the enclosing function to return aResult(or anotherFromResidualtype), this is for a function whose return type is already the bare error/Reportitself, because it never produces a value, only ever an eventual failure. - whatever
- Create a freeform report.
Structs§
- Backtrace
- A captured backtrace. See
Context::backtrace. - Context
- Everything about a
Reportother than its typed error: the narrative, structured fields, where and when it was created, its cause, and its factors. - Erased
Report - A
Reportwith its typed error erased, held as another report’s cause or as one of its factors. - Field
- Structured field attached to a report.
- Message
- A message describing what was being attempted, or what happened.
- Panicked
- A caught panic.
- Pretty
Panic Options - Options for
install_pretty_panic_hook_with. - Report
- A typed error (
E) together with everything else needed to report it: a narrative, structured fields, a backtrace, and, optionally, a cause and contributing factors. - Source
Location - Source location captured when a report or annotation is created.
- Suggestion
- Forward-looking, actionable advice, e.g., “did you forget to run
x init?”.
Enums§
- Annotation
- An entry in a report’s narrative.
- Value
- Structured value attached to a report field.
- Visibility
- Visibility of a structured field, controlling whether
Report::exportincludes its value.
Traits§
- Error
- Abstraction for types that can be carried by a
Report. - Error
Ext - Extension trait for bare errors.
- Into
Message - Conversion into a
Message. - Into
Suggestion - Conversion into a
Suggestion. - Result
Ext - Extension trait for results, implemented both for
Result<T, E>and forResult<T, Report<E>>. Every method returnsResult<T, Report<Err>>, which is a new type for the former and simplySelffor the latter. - Whatever
- Error type that opts into one-off, freeform diagnostic reports.
Functions§
- catch_
unwind - Run
f, catching a panic (if any) as aReport<Panicked>with a correct backtrace and location, instead of the onesstd::panic::catch_unwindalone can recover. - install_
panic_ hook - Install reportify’s panic-capturing hook now, rather than waiting for the first call
to
catch_unwind. - install_
pretty_ panic_ hook - Install reportify’s panic hook, additionally taking over how an uncaught panic
prints: instead of the default hook’s plain “thread panicked at” banner, an ordinary
panic (
.unwrap(),todo!(), a barepanic!(...), …) renders the same way aReportdoes: a styled headline, its location, and a frame-skipped backtrace/span trace if one was captured. - install_
pretty_ panic_ hook_ with - Like
install_pretty_panic_hook, withPrettyPanicOptionsto point at an issue tracker or attach fields, e.g., the application’s own version, to every rendered panic.