rtb-error 0.6.5

Error types and the diagnostic report pipeline. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Error and Result
description: Every variant of rtb_error::Error with its diagnostic code, help text and Display output, and what converts into it.
tags: [reference, api, errors]
---

# `Error` and `Result`

## The `Result` alias

```rust
pub type Result<T, E = Error> = std::result::Result<T, E>;
```

The default type parameter is the point: `rtb_error::Result<()>` means
`std::result::Result<(), rtb_error::Error>`, and `rtb_error::Result<(),
MyError>` still works when you want your own error type. It is a plain alias
with no behaviour of its own.

`rtb-cli` re-exports both under different names from its prelude
(`pub use rtb_error::{Error as RtbError, Result as RtbResult};`), so code
written against that framework sees `RtbResult<T>` for the same type.

## Every variant of `Error`

`Error` is `#[non_exhaustive]` and derives `Debug`, `thiserror::Error` and
`miette::Diagnostic`.

| Variant | Payload | `Display` | Diagnostic code | Help |
|---|---|---|---|---|
| `Config(String)` | the rejection message | `configuration error: {0}` | `rtb::config` | none |
| `Io(std::io::Error)` | the underlying I/O error | `I/O error: {0}` | `rtb::io` | none |
| `CommandNotFound(String)` | the name the user typed | `command not found: {0}` | `rtb::command_not_found` | `run `--help` to list available commands` |
| `FeatureDisabled(&'static str)` | the Cargo feature name | ``feature `{0}` is not compiled in`` | `rtb::feature_disabled` | `rebuild with the appropriate Cargo feature enabled` |
| `Other(Box<dyn Diagnostic + Send + Sync + 'static>)` | a downstream diagnostic | whatever the inner error displays | **the inner diagnostic's code** | the inner diagnostic's help |

Four of the five variants carry a code in the `rtb::` namespace. `Other` does
not. It is declared `#[diagnostic(transparent)]`, so every diagnostic facet
(code, help, severity, URL, labels, source code, related diagnostics) is read
straight off the boxed error. A rendered `Error::Other` never mentions
`rtb::other`, and there is a test asserting exactly that.

### Error::Other prints its message twice

`Other` is declared `#[error("{0}")] Other(#[from] Box<dyn Diagnostic + …>)`.
The `#[from]` also makes the boxed error the variant's `source`, so the
graphical renderer prints the message once as the diagnostic and once as its
cause:

```text
greet::no_name

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

It is cosmetic and there is no way to suppress it from the outside. The
simplest avoidance is not to box: a `Diagnostic` converts into a
`miette::Report` directly, so most code never needs `Error::Other` at all. See
[Define your own error type](../how-to/02-define-your-own-error-type.md#prefer-not-to-convert-at-all).

## What converts into `Error` automatically

Only two `From` impls exist, both generated by `#[from]`:

```rust
impl From<std::io::Error> for Error                                  // → Error::Io
impl From<Box<dyn Diagnostic + Send + Sync + 'static>> for Error     // → Error::Other
```

So `?` promotes an `std::io::Error` for free:

```rust
fn read(path: &Path) -> rtb_error::Result<String> {
    Ok(std::fs::read_to_string(path)?)   // → Error::Io
}
```

Your own error type does **not** convert automatically, even when it implements
`Diagnostic`. Box it at the boundary:

```rust
fn call() -> rtb_error::Result<()> {
    inner().map_err(|e| rtb_error::Error::Other(Box::new(e)))
}
```

`Box::new(e)` is coerced to `Box<dyn Diagnostic + Send + Sync + 'static>` at
the call site, so no turbofish is needed. The error type must be `Send +
Sync + 'static`, though, or the coercion fails to compile.

## Why matching on `Error` needs a wildcard arm

`#[non_exhaustive]` means a downstream `match` must always end with `_ =>`.
Without one the compiler rejects it:

```text
error[E0004]: non-exhaustive patterns: `_` not covered
   = note: `rtb_error::Error` is marked as non-exhaustive, so a wildcard `_`
     is necessary to match exhaustively
```

That is the intended behaviour, not a bug to work around: it is what lets a
new variant ship in a minor release without breaking you.

## What `Error` is not

`Error` is the errors the *application scaffolding* raises: a config source
rejecting a value, a command name that does not exist, a feature that was
compiled out. It is not a general-purpose error type for your program's
domain, and there is no variant to grow into for that. Define your own
`#[derive(Error, Diagnostic)]` enum and use `Other` only where the two worlds
meet. See [Define your own error type](../how-to/02-define-your-own-error-type.md).

## Traits `Error` implements

`Debug`, `Display`, `std::error::Error`, `miette::Diagnostic`, and
`Send + Sync + 'static` (because every variant's payload is). Those bounds are
what allow an `Error` to cross a thread boundary and to be converted into a
`miette::Report`, and there is a test that fails to compile if they are ever
lost.

`Error` is **not** `Clone` and not `PartialEq`. `std::io::Error` is neither, and
a boxed trait object cannot be either. Compare on the diagnostic code or the
`Display` string if you need to assert on one in a test.