---
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 — 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:
```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: {:?}` — 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`](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 — 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.
### 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:
| 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.