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.
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:
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¶
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.
How the error got to main |
exit_code_of |
|---|---|
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:
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 for the
whole pattern.