---
title: rtb-error
description: Canonical Error enum, Result alias, and the miette diagnostic-hook pipeline every rtb-* crate funnels through.
---
# `rtb-error`
`rtb-error` is the error-handling foundation of the Rust Tool Base
framework. It defines the canonical [`Error`](#error) enum, the
`Result<T, E = Error>` type alias, and the process-edge diagnostic
pipeline built on [`miette`][miette] and [`thiserror`][thiserror].
Every other `rtb-*` crate imports it; every downstream tool renders
errors through it.
Part of the [phpboyscout Rust toolkit](https://rust.phpboyscout.uk);
extracted from — and battle-tested by —
[rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base).
## Overview
Go Tool Base threads an `ErrorHandler.Check()` funnel through every
command handler. `rtb-error` deliberately **does not** do that —
errors are values, propagated with `?`, and rendered once at `main()`
via an installed `miette` hook. This is the idiomatic Rust pattern;
the funnel approach is a Go paradigm the framework explicitly rejects
(framework spec, Appendix B).
## Design rationale
Three decisions shape this crate:
1. **`thiserror` for authoring, `miette::Diagnostic` for rendering.**
Every public enum derives both. `thiserror` gives us `#[error(...)]`
and `#[from]`; `miette` gives us `code(...)`, `help(...)`, source
spans, and the terminal renderer.
2. **`#[non_exhaustive]` everywhere.** Adding a variant to a public
error enum must be a minor-version change, not a breaking one.
Downstream `match` arms must always carry a wildcard.
3. **Mutable footer, immutable hook.** `miette::set_hook` is a
`OnceLock`-backed set-once function; calling it twice fails with
`InstallError`. `rtb-error` installs its hook once, then reads
the footer from its own `RwLock<Option<Footer>>` at render time
— so callers can update the footer freely without touching
miette's global.
## Core types
### `Error`
```rust
#[derive(Debug, Error, Diagnostic)]
#[non_exhaustive]
pub enum Error {
#[error("configuration error: {0}")]
#[diagnostic(code(rtb::config))]
Config(String),
#[error("I/O error: {0}")]
#[diagnostic(code(rtb::io))]
Io(#[from] std::io::Error),
#[error("command not found: {0}")]
#[diagnostic(
code(rtb::command_not_found),
help("run `--help` to list available commands"),
)]
CommandNotFound(String),
#[error("feature `{0}` is not compiled in")]
#[diagnostic(
code(rtb::feature_disabled),
help("rebuild with the appropriate Cargo feature enabled"),
)]
FeatureDisabled(&'static str),
/// Escape hatch for downstream-crate diagnostics.
#[error("{0}")]
#[diagnostic(transparent)]
Other(#[from] Box<dyn Diagnostic + Send + Sync + 'static>),
}
```
Every variant carries a `code` under the `rtb::` namespace. The
`Other` variant is the escape hatch: downstream crates define their
own `thiserror::Error + miette::Diagnostic` enums and box them in
here at boundaries.
### `Result`
```rust
pub type Result<T, E = Error> = std::result::Result<T, E>;
```
Used everywhere inside the framework. Downstream tools typically
alias `rtb_error::Result` as `RtbResult` via the `rtb_cli::prelude`.
### `hook` module
```rust
pub mod hook {
/// Install the default graphical report handler.
/// Idempotent; first caller wins (miette's hook is set-once).
pub fn install_report_handler();
/// Install miette's panic hook. Idempotent —
/// `std::panic::set_hook` overwrites on every call.
pub fn install_panic_hook();
/// Install a footer closure read on every diagnostic render.
/// Safe to call multiple times; the most recent closure wins.
pub fn install_with_footer<F>(footer: F)
where
F: Fn() -> String + Send + Sync + 'static;
}
```
`rtb_cli::Application::run` calls all three during startup, with the
footer sourced from `ToolMetadata::help.footer()`.
Full API reference (including the `exit_code` module):
[docs.rs/rtb-error](https://docs.rs/rtb-error).
## Usage patterns
### Authoring a downstream error enum
```rust
use miette::Diagnostic;
use thiserror::Error;
#[derive(Debug, Error, Diagnostic)]
#[non_exhaustive]
pub enum MyCrateError {
#[error("bad config value {name}: {value}")]
#[diagnostic(
code(mytool::bad_value),
help("pick a value between 1 and 65535"),
)]
BadValue { name: String, value: String },
}
// Box across crate boundaries:
fn returns_framework_result() -> rtb_error::Result<()> {
Err(MyCrateError::BadValue { .. }).map_err(|e| {
rtb_error::Error::Other(Box::new(e))
})
}
```
### Ad-hoc diagnostics
```rust
return Err(miette::miette!(
code = "mytool::no_config",
help = "run `mytool init` first",
"no config file at {}",
path.display()
));
```
### `main()` wiring
`rtb_cli::Application::run` already does the install; tools that
bypass `Application` install manually:
```rust
#[tokio::main]
async fn main() -> miette::Result<()> {
rtb_error::hook::install_report_handler();
rtb_error::hook::install_panic_hook();
// ...
}
```
## Hook safety
!!! warning "Footer closures must not panic"
The installed footer closure is invoked on every render. A
panicking closure is caught via `catch_unwind` and the footer is
silently suppressed for that render, but the framework logs a
diagnostic about it. Thread-local re-entry guard prevents a
panicking footer from recursing through miette's panic hook and
producing a double-panic abort.
See [Engineering Standards §1.3](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/engineering-standards.md#13-hook-and-panic-hook-safety)
for the full rules around hook + panic-hook safety.
## Consumers
| [rtb-app](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/components/rtb-app.md) | `Error` for `FeatureDisabled` / `CommandNotFound` variants. |
| [rtb-config](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/components/rtb-config.md) | Converts `ConfigError::Parse` into `Error::Other`. |
| [rtb-cli](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/components/rtb-cli.md) | `Application::run` installs all three hooks + wires the `ToolMetadata::help` footer. |
| [rtb-credentials](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/components/rtb-credentials.md) | `CredentialError` is Boxed into `Error::Other` at the app boundary. |
| [rtb-telemetry](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/components/rtb-telemetry.md) | `TelemetryError` likewise. |
## Testing
20 acceptance criteria across:
- 13 unit tests (`tests/unit.rs`) — T1–T13 including panic resilience.
- 6 Gherkin scenarios (`tests/features/error.feature`).
- 1 trybuild fixture — exhaustive `match` on the `#[non_exhaustive]`
enum must fail to compile.
## Design record
The authoritative contract is the crate's v0.1 spec, retained in the
rust-tool-base
[spec series](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/2026-04-22-rtb-error-v0.1.md).
[miette]: https://crates.io/crates/miette
[thiserror]: https://crates.io/crates/thiserror