rtb-error 0.6.5

Error types and the diagnostic report pipeline. Part of the phpboyscout Rust toolkit.
Documentation
---
title: hook module
description: The three install functions, the ordering rule that decides whether they take effect, and exactly what the installed handler renders.
tags: [reference, api, hook]
---

# `hook` module

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

```rust
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. `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:

```rust
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: {:?}`. Panics therefore 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`]04-exit-code.md.

## `install_with_footer`

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, such as a request ID or 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.

### Footer output shape

```text
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. The table was checked against the
`miette` 7.6.0 source; a `miette` release can move any row.

| 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 and 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.