rtb-error 0.6.5

Error types and the diagnostic report pipeline. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Set the process exit code
description: Attach a specific exit code to an error and read it back at the boundary, and the wrapping that silently loses it.
tags: [how-to, exit-code]
---

# Set the process exit code

By default a tool exits `1` on any error. To exit with something else (`64`
for a usage error, `77` for a permission failure, whatever your callers
expect), attach the code to the error and read it back in `main`.

## Attach the code where the error is raised

```rust
use rtb_error::WithExitCode;

fn parse_args() -> miette::Result<Args> {
    let name = std::env::args().nth(1)
        .ok_or(GreetError::NoName.with_exit_code(64))?;
    Ok(Args { name })
}
```

`with_exit_code` is available on any `Diagnostic + Send + Sync + 'static`. It
wraps the error in an `ExitCoded`, which renders exactly as the original did.
Nothing about the diagnostic on screen changes.

## Read it back in `main`

`main` has to return `std::process::ExitCode`. A `main` returning
`miette::Result<()>` exits `1` on every error, so the attached code would never
be applied:

```rust
use std::process::ExitCode;
use rtb_error::exit_code_of;

fn main() -> ExitCode {
    rtb_error::hook::install_report_handler();
    rtb_error::hook::install_panic_hook();

    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(report) => {
            eprintln!("Error: {report:?}");
            ExitCode::from(exit_code_of(&report).unwrap_or(1))
        }
    }
}
```

Two details in that block are load-bearing. It is `{report:?}` and not
`{report}`, because `Debug` is what routes through the installed hook and
`Display` prints the bare message. And `unwrap_or(1)` is your default for
every error that carries no code, which is most of them.

Verify it with the shell:

```console
$ greet
Error: greet::no_name

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

$ echo $?
64
```

## Do not box an `ExitCoded` into `Error::Other`

`exit_code_of` is a downcast. This loses the code, silently:

```rust
let coded = MyError::Bad.with_exit_code(64);
let err = rtb_error::Error::Other(Box::new(coded));   // code is now unreachable
```

The report downcasts to `rtb_error::Error`, not to `ExitCoded`, so
`exit_code_of` returns `None` and the boundary falls back to `1`. Attach the
code at the outermost layer, after any conversion into `Error`.

`Report::wrap_err` is safe. Adding context does not lose the code.

## Do not use `0`

`with_exit_code(0)` compiles and is applied. The process then reports success
while printing a diagnostic, which is almost always a bug in the caller. There
is no check for it.

## Which codes to use

`rtb-error` has no opinion. The type is `u8`, and `0` to `255` are all
accepted. If your tool has no existing convention,
[`sysexits.h`](https://man.freebsd.org/cgi/man.cgi?sysexits) is the usual
starting point: `64` for a usage error, `66` for a missing input file, `77` for
a permission failure. Whatever you choose, document it in your own tool's
reference, because a caller scripting around your exit codes cannot discover
them from the binary.