---
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.