Environment and Process Management

Exit Status

An ExitStatus (std::process::ExitStatus) represents the termination state of a finished child process. It indicates whether the process succeeded (code == 0) or failed, and whether it exited normally via an exit code or was terminated by an operating system signal.

io::Result<ExitStatus> vs ExitStatus::success()

When running a command via status(), output(), or wait(), there are two distinct layers of success or failure:

Command::new("tool").status()
  |
  +-- Err(io::Error)       OS failed to launch the binary (e.g., NotFound, PermissionDenied)
  |
  +-- Ok(ExitStatus)       Binary launched and ran to completion (or signal death)
        |
        +-- status.success() == true    Process exited with code 0
        +-- status.success() == false   Process exited with non-zero code OR died via signal
Method Return Type Meaning
status.success() bool Returns true if and only if the process exited normally with exit code 0.
status.code() Option<i32> Returns Some(code) if the process exited normally, or None on Unix if terminated by a signal.
ExitStatusExt::signal(&status) Option<i32> (Unix only) Returns Some(sig) if the child was killed by signal sig (e.g., 9 for SIGKILL).

Inspecting Exit Codes

use std::io;
use std::process::{Command, Stdio};

fn main() -> io::Result<()> {
    // Passing `--version` succeeds with exit code 0
    let ok_status = Command::new("rustc")
        .arg("--version")
        .stdout(Stdio::null())
        .status()?;

    println!("Success: {}, Code: {:?}", ok_status.success(), ok_status.code());

    // Passing an invalid flag fails with a non-zero exit code (still Ok(ExitStatus)!)
    let err_status = Command::new("rustc")
        .arg("--definitely-not-a-valid-rustc-flag")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()?;

    println!("Success: {}, Code: {:?}", err_status.success(), err_status.code());

    Ok(())
}
Success: true, Code: Some(0)
Success: false, Code: Some(1)

When status.code() Returns None on Unix

On Windows, every terminated process has a 32-bit exit code, so status.code() always returns Some(i32). On Unix, the wait status word distinguishes normal exits (_exit(code)) from signal deaths (SIGTERM, SIGKILL, SIGSEGV). When a Unix child is killed by a signal, status.code() returns None:

use std::io;
use std::process::{Command, Stdio};

fn main() -> io::Result<()> {
    // Spawn a child waiting on a piped stdin so it stays alive until killed
    let mut child = Command::new("rustc")
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;

    // Forcefully terminate the child (`SIGKILL` on Unix, `TerminateProcess` on Windows)
    child.kill()?;
    let status = child.wait()?;

    println!("Killed child success(): {}", status.success());

    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        println!("Unix code(): {:?}, signal(): {:?}", status.code(), status.signal());
    }

    #[cfg(windows)]
    {
        println!("Windows code() is Some: {}", status.code().is_some());
    }

    Ok(())
}
Killed child success(): false
Unix code(): None, signal(): Some(9)