Skip to content

Produce user-facing errors

RTB treats errors as values: derive them, propagate with ?, and let the edge report them once. There is no ErrorHandler.check() funnel.

In library crates

Every public crate derives both thiserror::Error and miette::Diagnostic on its error enum:

use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Error, Diagnostic)]
pub enum DeployError {
    #[error("no target environment configured")]
    #[diagnostic(
        code(mytool::deploy::no_target),
        help("set `deploy.target` in your config or pass --target")
    )]
    NoTarget,

    #[error("upload failed")]
    #[diagnostic(code(mytool::deploy::upload))]
    Upload(#[source] std::io::Error),
}

Do not use anyhow inside framework crates — it has no diagnostic surface and ambiguous provenance. (anyhow in tests or examples is fine.)

In command / application code

Return miette::Result<T> and propagate with ?:

async fn run(&self, app: App) -> miette::Result<()> {
    let target = resolve_target(&app)?; // DeployError -> miette via ?
    Ok(())
}

rtb-cli::Application::run installs miette::set_hook (with the tool's help channel appended) and a panic hook, so a returned error is rendered with its code, help, and source chain automatically.

Ad-hoc hints

For one-off diagnostics without a dedicated enum variant:

return Err(miette::miette!(
    code = "mytool::deploy::locked",
    help = "another deploy is in progress; retry shortly",
    "deploy lock held by pid {pid}",
));

See rtb-error for the canonical hook pipeline and Engineering Standards §3 for the full error-handling contract.