Environment and Process Management

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