---
title: exit_code module
description: How ExitCoded, WithExitCode and exit_code_of carry a process exit code on an error, and the wrapping that loses it.
tags: [reference, api, exit-code]
---
# `exit_code` module
A process exits `1` on any error unless something says otherwise. This module
is the "otherwise": a wrapper that carries a `u8` alongside a diagnostic, and a
function that reads it back at the process boundary.
```rust
pub struct ExitCoded { /* private */ }
impl ExitCoded {
pub fn new(code: u8, source: impl Diagnostic + Send + Sync + 'static) -> Self;
pub const fn exit_code(&self) -> u8;
}
pub trait WithExitCode: Sized {
fn with_exit_code(self, code: u8) -> ExitCoded;
}
impl<E: Diagnostic + Send + Sync + 'static> WithExitCode for E { /* … */ }
pub fn exit_code_of(report: &Report) -> Option<u8>;
```
## `WithExitCode::with_exit_code`
A blanket impl, so every `Diagnostic + Send + Sync + 'static` gets the method:
```rust
use rtb_error::WithExitCode;
return Err(MyError::BadInput.with_exit_code(64));
```
The code is a `u8`, matching `std::process::ExitCode::from`. Values above 255
are a compile error rather than a silent truncation.
**`0` is accepted and is almost never what you want.** `with_exit_code(0)`
attaches "success" to an error, and a boundary that does
`ExitCode::from(exit_code_of(&report).unwrap_or(1))` will report success while
printing a diagnostic. Nothing rejects it.
## `ExitCoded` renders as though it were not there
`ExitCoded` delegates `Display`, `Debug`, `Error::source`, and every
`Diagnostic` method (`code`, `severity`, `help`, `url`, `source_code`,
`labels`, `related`, `diagnostic_source`) to the wrapped error. Attaching a
code never changes a single character of the rendered diagnostic. The code is
metadata for the process boundary and invisible to the user.
## `exit_code_of`
```rust
pub fn exit_code_of(report: &Report) -> Option<u8>
```
Returns `Some(code)` when the report's error downcasts to `ExitCoded`, and
`None` for an ordinary error, at which point the boundary applies its own
default, conventionally `1`.
## When does the exit code survive?
`exit_code_of` is a downcast, so it depends on how the error reached the
`Report`.
| `ExitCoded` converted straight into a `Report` (`?` into `miette::Result`) | **`Some(code)`** |
| `ExitCoded` propagated through several `?` in functions returning `miette::Result` | **`Some(code)`** |
| `Report::wrap_err("context")` applied on top | **`Some(code)`** |
| Boxed into `rtb_error::Error::Other(Box::new(coded))` | **`None`**: the code is lost |
The last row is the one that bites, and it is not obvious from the types. There
is no `From<ExitCoded> for rtb_error::Error`, so wrapping is always something
you wrote deliberately. But once the `ExitCoded` is inside an `Error::Other`,
the report downcasts to `Error`, not to `ExitCoded`, and the code silently
becomes the default. Attach the exit code at the outermost layer, after any
conversion into `Error`, not before.
## Reading it at the boundary
`rtb-error` provides no `main` wrapper. The boundary is yours to write, and it
needs to return `ExitCode` rather than `miette::Result`, because
`miette::Result` always exits `1`:
```rust
use std::process::ExitCode;
use rtb_error::exit_code_of;
fn main() -> ExitCode {
rtb_error::hook::install_report_handler();
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(report) => {
eprintln!("Error: {report:?}");
ExitCode::from(exit_code_of(&report).unwrap_or(1))
}
}
}
```
The `{report:?}` is deliberate: `Report`'s `Debug` implementation is what routes
through the installed hook. `{report}` prints the bare `Display` message with no
code, no help and no footer. See
[Set the process exit code](../how-to/03-set-the-process-exit-code.md) for the
whole pattern.