Skip to content

Error and Result

The Result alias

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--helpto 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:

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.

What converts into Error automatically

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

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:

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:

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 — but the error type must be Send + Sync + 'static 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:

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.

Traits Error implements

Debug, Display, std::error::Error, miette::Diagnostic, and — because every variant's payload is — Send + Sync + 'static. 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.