🎓 A1Learner
🔍

Rust Exit Codes

An exit code is a number a program returns to the operating system when it ends. Code 0 means success.

main returns the code

Give main the ExitCode return type, and return the code you choose.

use std::process::ExitCode;
fn main() -> ExitCode {
    ExitCode::SUCCESS
}

process::exit skips cleanup

The Guard's Drop would print a line if it ran. It never runs.

struct Guard;
impl Drop for Guard {
    fn drop(&mut self) {
        println!("guard dropped: files flushed");
    }
}
fn main() {
    let _g = Guard;
    println!("about to exit");
    std::process::exit(3);
}

Returning runs the cleanup

The same Guard, but main returns a code instead of calling exit. This time the Drop line prints. The process still exits with code 3.

fn main() -> ExitCode {
    let _g = Guard;
    println!("about to return");
    ExitCode::from(3)
}

A different code per failure

Match on the situation, return the code that names it.

match args.get(1).map(|s| s.as_str()) {
    None => ExitCode::from(2),       // wrong usage
    Some("boom") => ExitCode::FAILURE, // the job failed
    Some(_) => ExitCode::SUCCESS,      // all good
}

Map each failure to its code

One small function owns the mapping. main stays free of bare numbers. Run its three ways and the codes come out 2, 1, and 0.

fn code_for(fail: &Fail) -> ExitCode {
    match fail {
        Fail::Usage => ExitCode::from(2),
        Fail::Job => ExitCode::from(1),
    }
}

Example

use std::process::ExitCode;

fn run(args: &[String]) -> ExitCode {
    match args.get(1).map(|s| s.as_str()) {
        None => {
            eprintln!("usage: greeter <name>");
            ExitCode::from(2)
        }
        Some("boom") => {
            eprintln!("error: the name 'boom' is banned");
            ExitCode::FAILURE
        }
        Some(name) => {
            println!("hello, {name}");
            ExitCode::SUCCESS
        }
    }
}

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().collect();
    run(&args)
}

Key points

  • ExitCode works because main may return any Termination type.
  • The same trait is why returning a Result from main works too.
  • Reach for process::exit only in a deep helper that cannot return to main.
  • Use process::exit only when skipping cleanup is safe there.
  • Two usage conventions exist.
  • Clap and most GNU tools use 2 for a usage error.
  • The older sysexits list starts its special codes at 64.
  • Document your codes in --help or the README.
  • A code nobody can look up is just a number.
  • Scripts read the code to tell a usage mistake from a failed job.

In this example, main returns whatever run decides, and run sorts the call into three cases. Run with no name, the program prints usage: greeter <name> (to stderr) and exits with code 2 — wrong usage. Run it as greeter ada and it prints hello, ada and exits 0. Run it as greeter boom and it prints its error and exits 1. All three runs were tried for real: the codes were 2, 0 and 1, exactly as the code promises. A shell script can now react to each case on its own.

Note

An Err returned from main always exits with code 1 and prints Error: first.

ExitCode is for when the number itself must carry meaning.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why prefer returning ExitCode from main over calling process::exit?