Skip to content

hook module

Three functions, all returning (), all safe to call any number of times in any order:

pub fn install_report_handler();
pub fn install_panic_hook();
pub fn install_with_footer<F>(footer: F)
where
    F: Fn() -> String + Send + Sync + 'static;

When must the hook be installed?

Before the first miette::Report in the process exists. This is the one rule that catches people, and getting it wrong fails silently.

miette keeps its error hook in a process-global OnceLock. Constructing a Report calls HOOK.get_or_init(…), which installs miette's default handler if nothing is there yet. From that moment the slot is full and miette::set_hook returns InstallError forever — so install_report_handler becomes a no-op, and any footer you register is never read.

Creating the Report is enough. You do not have to render it, print it, or even keep it:

let _ = miette::Report::new(SomeError);   // hook slot is now sealed
rtb_error::hook::install_with_footer(|| "support: …".into());
// the footer will never appear

Put the install calls at the top of main, before any fallible work. If a library you depend on builds a Report during its own initialisation, it wins and there is nothing this crate can do about it.

install_report_handler

Installs rtb-error's ReportHandler as miette's global hook.

  • Idempotent. Calling it twice is harmless; the second call's miette::set_hook result is discarded.
  • First caller wins. If a hook is already installed — by an earlier call, by install_with_footer, by miette::set_hook in your own code, or by miette itself as described above — this call does nothing and the existing hook stays.
  • No return value and no error. There is no way to ask whether it took effect. If you need certainty, call it as the first statement in main.

install_panic_hook

Calls miette::set_panic_hook, which replaces the standard panic hook with one that builds a Report from the panic payload and prints it to stderr with Error: {:?} — so panics render through whichever miette hook is installed, including this crate's, footer and all.

  • Idempotent, and last caller wins. std::panic::set_hook overwrites unconditionally, unlike miette::set_hook. Calling this after your own set_hook discards yours.
  • The rendered panic carries the panic message, the source location as a cause, and the fixed help line set the `RUST_BACKTRACE=1` environment variable to display a backtrace. Setting RUST_BACKTRACE=1 adds the backtrace to the message.
  • catch_unwind still works. Installing the hook changes what is printed, not whether the panic unwinds.
  • The exit code is unchanged — a panicking process still exits 101. The panic hook has no interaction with exit_code.

Installs the report handler (via install_report_handler, with the same first-caller-wins rule) and registers a closure whose output is appended to every rendered diagnostic.

  • The closure runs on every render, not once at install time. It can return different text each time — a request ID, a support channel that depends on config loaded later.
  • Replacing the footer is allowed; the most recent call wins. This is the reason the crate wraps miette's set-once hook rather than using it directly.
  • Returning an empty string suppresses the footer for that render. No blank line is emitted.
  • A panicking closure is caught and the footer is dropped for that render. The diagnostic body still renders. Nothing is logged about the suppression — if the panic hook is installed it will print the panic itself, and if it is not the panic is silent. A footer closure that panics is therefore very easy to miss; keep the closure trivial.
  • The footer is separated from the diagnostic by one blank line, and followed by a newline.
Error: greet::no_name

  x no name given
  help: pass a name: `greet Ada`

Report bugs at https://example.invalid/greet/issues

What changes about rendering when you install this handler

rtb-error installs miette::GraphicalReportHandler::new() and appends the footer. miette's own default is MietteHandler, which is configured differently. Installing this crate's handler changes three things, and all three are worth knowing before you do it:

Behaviour miette default (MietteHandler) After install_report_handler
Wrap width terminal width, or 80 when it cannot be detected fixed at 200 columns
NO_GRAPHICS env var any value other than 0 switches to the narratable (screen-reader friendly) renderer ignored; always graphical
Terminal hyperlinks emitted only when the terminal is detected as supporting OSC-8 always emitted for diagnostics carrying a url(…)

Colour and Unicode box-drawing still adapt: GraphicalTheme::default() falls back to plain ASCII with no colour when stdout or stderr is not a terminal, and honours NO_COLOR.

The practical effect of the width row is that long diagnostic messages wrap at 200 columns rather than at your terminal, so in an 80-column terminal they wrap twice — once by the handler, once by the terminal. There is no option to change it; the width is hard-coded in GraphicalReportHandler::new().

Thread safety

The footer slot is a static OnceLock<RwLock<Option<Footer>>>. Installs take the write lock, renders take the read lock, and the read guard is released before anything is written to the formatter. The footer closure is required to be Send + Sync + 'static because it is called from whichever thread is rendering.

If the lock is poisoned, install_with_footer panics with footer lock poisoned — another thread panicked mid-update, and rendering silently skips the footer rather than panicking a second time.