Skip to content

Define your own error type

Add the derives

Your crate needs thiserror and miette as direct dependencies — rtb-error re-exports the Diagnostic trait and derive, but not thiserror::Error.

[dependencies]
rtb-error = "0.6"
miette = "7"
thiserror = "2"
use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Error, Diagnostic)]
#[non_exhaustive]
pub enum GreetError {
    #[error("no name given")]
    #[diagnostic(code(greet::no_name), help("pass a name: `greet Ada`"))]
    NoName,

    #[error("greeting file {path} is unreadable")]
    #[diagnostic(code(greet::unreadable), help("check the path and permissions"))]
    Unreadable {
        path: String,
        #[source]
        source: std::io::Error,
    },
}

#[error(...)] is the one-line message. code(...) is the machine-readable identity a user can search for. help(...) is the line telling them what to do next — write it as an instruction, not as a restatement of the problem.

Choose a code namespace

Use your tool's name as the first segment: greet::no_name, not rtb::no_name. The rtb:: namespace belongs to rtb_error::Error and its four scaffolding variants; reusing it makes two unrelated failures look like the same one in a support conversation.

Mark the enum #[non_exhaustive]

Not required, but it is what the rest of the toolkit does, and for the same reason: adding a variant later stops being a breaking change. The cost is that callers must carry a wildcard match arm from day one.

Convert at the framework boundary

rtb_error::Error has exactly two automatic conversions — From<std::io::Error> and From<Box<dyn Diagnostic + Send + Sync + 'static>>. Your error is not one of them, so box it explicitly:

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

Box::new(e) coerces to the trait object at the call site. The error type must be Send + Sync + 'static; if it is not, that line is where you find out.

Because Error::Other is #[diagnostic(transparent)], the user still sees greet::no_name and your help text. The wrapper never appears.

Prefer not to convert at all

Boxing into Error::Other is for code that has to hand something back to framework scaffolding expecting an rtb_error::Error. Most functions do not. If yours returns to your own main, return your own type and let ? convert it into a miette::Report directly:

fn run() -> miette::Result<()> {
    greet(std::env::args().nth(1))?;   // GreetError → Report, no boxing
    Ok(())
}

miette::Report accepts any Diagnostic + Send + Sync + 'static, so the umbrella enum is not on the path at all. Two things improve as a result: any attached exit code stays readable, which boxing into Error::Other would destroy, and the message is printed once rather than twice.

Raise a one-off diagnostic without a type

For a failure that happens in exactly one place and does not need a name, the miette! macro builds a Report inline:

return Err(miette::miette!(
    code = "greet::no_config",
    help = "run `greet init` first",
    "no config file at {}",
    path.display()
));

It is a shortcut, not a replacement. An error a user might report to you should have a code they can search for and a type you can match on.