Skip to content

Why the hook is installed once and the footer is not

The constraint: miette's hook can only be set once

miette stores its error hook in a static OnceLock. miette::set_hook succeeds on the first call and returns InstallError on every call after it. That is a reasonable design for a global: it makes rendering behaviour predictable, because nothing can change it out from under a running program.

It is also awkward for a framework. A tool's support footer — "report bugs at …" — is not known when main starts. It comes from configuration, or from metadata that is assembled a few frames later. If the footer had to be baked into the handler at install time, the framework would have to defer installing until it knew the footer, which is exactly the window in which an early error could be raised and rendered without one.

The resolution: an indirection the handler owns

So rtb-error installs a handler that does not hold the footer. It holds a reference to a slot:

static FOOTER: OnceLock<RwLock<Option<Footer>>> = OnceLock::new();

The handler delegates the structural render to miette's GraphicalReportHandler, then reads that slot at render time and appends whatever it finds. miette's global is written once, as miette intends. The footer is written as often as you like, because it lives in a slot this crate owns.

The visible result is that all three install_* functions can be called in any order, any number of times, and the most recent footer is the one that appears. That property is the whole reason the wrapper exists — without it, the API would have to be "install once, and get it right".

Why installing early matters more than it looks

The same OnceLock that makes the wrapper necessary also creates the crate's sharpest edge, and it is worth understanding rather than memorising.

miette does not install its default handler eagerly. It installs it lazily, the first time a Report is constructed, via HOOK.get_or_init(…). So the slot is not filled at program start; it is filled by whichever comes first — your install call, or the first error anywhere in the process.

Lose that race and everything downstream is silent. install_report_handler still returns (). install_with_footer still stores the closure. The closure is simply never called, because the handler that would have called it was never installed. There is no error, no warning, and no way to interrogate the state afterwards — miette exposes no "is a hook installed" predicate.

This is a genuine trade. A fallible install_report_handler() -> Result<(), _> would surface the problem, but it would also force every caller to handle a failure they cannot do anything about, and it would make the idempotent "call it twice, it's fine" contract impossible to state. The crate chose the quiet API and the strict ordering rule. The rule is: install first, before any fallible work.

The footer is a caller-supplied closure that runs deep inside a render, while a read lock is held, potentially while a panic is already being reported. Two things go wrong if it panics and nothing guards it.

The panic propagates out of a Display-style path where callers are not expecting one. And if miette's panic hook is installed, that hook renders the panic through this same handler, which calls the same panicking closure again — a double panic, which aborts the process.

So the handler wraps the call in catch_unwind, releases the read guard before writing anything to the formatter, and sets a thread-local re-entry flag that makes a nested render skip the footer step entirely. Those three measures are the toolkit's engineering standard for any framework-global handler that calls user code — rust-tool-base engineering standards §1.3 — and this handler is the reference implementation named there.

The cost of that safety is silence: a footer closure that panics produces no footer and no complaint from this crate. Keep the closure trivial — format a string that is already in hand, do not compute anything that can fail.

Why the handler is GraphicalReportHandler and not miette's default

miette's default hook builds a MietteHandler, which inspects the environment: terminal width, hyperlink support, NO_GRAPHICS for the narratable renderer. rtb-error installs a GraphicalReportHandler directly, because the footer has to be appended around a handler whose rendering entry point this crate can call, and GraphicalReportHandler::render_report is that entry point.

The consequence is that the environment detection MietteHandler performs is lost. Width becomes a fixed 200 columns, NO_GRAPHICS stops switching to the narratable renderer, and hyperlinks are always emitted. Those differences are tabulated in the hook reference; they are a side effect of the wrapping, not a decision anybody made about how diagnostics should look.