Exit Codes and Error Reporting
An exit code is an integer returned by a process to its parent upon termination, where 0 universally signals success and non-zero values (1–255) signal specific failure modes. In Rust, fn main() can return any type implementing the std::process::Termination trait—including (), std::process::ExitCode, and Result<T, E>—allowing structured error reporting while running all stack destructors.
fn main() -> impl Termination
|
v
Termination::report(self) -> ExitCode
|
+--------------+--------------+
| |
v v
ExitCode::SUCCESS (0) ExitCode::from(u8) (1..=255)
Standard main Return Types Implementing Termination
Return Type of fn main() |
Ok / Normal Behavior |
Err / Failure Behavior |
|---|---|---|
() |
Returns ExitCode::SUCCESS (0). |
Only fails if a panic occurs (101). |
ExitCode |
Returns the exact ExitCode value (0..=255). |
Returns the exact ExitCode value (0..=255). |
Result<(), E: Debug> |
Returns ExitCode::SUCCESS (0). |
Prints "Error: {err:?}" to stderr and returns ExitCode::FAILURE (1). |
Result<ExitCode, E: Debug> |
Returns the inner ExitCode. |
Prints "Error: {err:?}" to stderr and returns ExitCode::FAILURE (1). |
Returning ExitCode and Custom u8 Codes
ExitCode wraps a single u8 because POSIX operating systems only preserve the lowest 8 bits (status & 0xFF) of a process's exit code.
use std::process::ExitCode;
fn verify_input(input: &str) -> Result<(), u8> {
if input.is_empty() {
return Err(64); // EX_USAGE (BSD sysexits convention)
}
if !input.starts_with("v1:") {
return Err(65); // EX_DATAERR
}
Ok(())
}
fn main() -> ExitCode {
match verify_input("v1:payload") {
Ok(()) => {
println!("Input verified; returning SUCCESS");
ExitCode::SUCCESS
}
Err(code) => {
eprintln!("Verification failed with exit code {code}");
ExitCode::from(code)
}
}
}
Input verified; returning SUCCESS
Implementing Termination for a Custom CLI Result Type
When returning Result<(), E> from main(), the standard library formats the error using Debug ({:?}) rather than Display ({}) and always exits with code 1. To print user-friendly Display diagnostics to stderr and map different error variants to distinct exit codes, implement Termination on a custom wrapper type:
use std::fmt;
use std::process::{ExitCode, Termination};
enum CliError {
InvalidUsage(String),
ConfigMissing(String),
IoFailure(std::io::Error),
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::InvalidUsage(msg) => write!(f, "usage error: {msg}"),
CliError::ConfigMissing(path) => write!(f, "missing config file: {path}"),
CliError::IoFailure(err) => write!(f, "I/O error: {err}"),
}
}
}
struct CliOutcome(Result<(), CliError>);
impl Termination for CliOutcome {
fn report(self) -> ExitCode {
match self.0 {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("error: {err}");
let code = match err {
CliError::InvalidUsage(_) => 2,
CliError::ConfigMissing(_) => 78,
CliError::IoFailure(_) => 74,
};
ExitCode::from(code)
}
}
}
}
fn execute() -> Result<(), CliError> {
println!("Application executed cleanly");
Ok(())
}
fn main() -> CliOutcome {
CliOutcome(execute())
}
Application executed cleanly