7.8Halting your program early

Last updated June 12, 2026

Lesson 2.2 left an IOU: since main returns (), your program signals "success" automatically when main ends, and the tools for signaling anything else were promised for this lesson (and lesson 12.3). Time to pay.

Exit codes

When your program ends, it hands the operating system a number called an exit code (or status code): 0 means "succeeded," anything else means "failed," with the specific nonzero value left for the program to define. Your shell keeps the last program's exit code in a variable, so you can see this handshake directly. $? holds it:

$ cargo run
   Compiling hello_world v0.1.0 (/Users/you/projects/hello_world)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/hello_world`
Hello, world!
$ echo $?
0

Your main returned normally, so the code was 0. Nobody reads these numbers at a terminal; other programs read them. Shell scripts, build systems, and CI pipelines chain programs together with "if the last one succeeded, continue," and exit codes are the entire vocabulary of that conversation. Every cargo build you've run has been both speaking and listening to it.

Ending the program from anywhere: std::process::exit

Returning from main is the normal ending. std::process::exit(code) is the early one: it ends the program immediately, from any function, with the exit code you pass.

use std::process;

fn main() {
    println!("checking the launch conditions...");
    let fuel = 20;
    if fuel < 50 {
        println!("not enough fuel, scrubbing the launch");
        process::exit(1);
    }
    println!("liftoff!");   // never reached today
}
checking the launch conditions...
not enough fuel, scrubbing the launch
$ echo $?
1

The word immediately is carrying weight. exit doesn't return to its caller, doesn't finish main, and skips the program's tidy shutdown entirely: no value gets dropped (lesson 8.3 gives "dropped" its real meaning, and this sentence will retroactively say more than it does today). Rust does make one accommodation on the way out the door: the standard output buffer gets flushed, so everything you've printed is delivered even when exit cuts the program short. Other buffered destinations get no such favor; when chapter 20 has you writing to files through a buffer, an exit will abandon whatever hasn't been written yet, precisely because the cleanup that would have saved it never runs. If a program with an exit in it produces a mysteriously truncated file one day, you'll know the suspect.

Best practice

Prefer returning from main as your program's ending; use std::process::exit only when you're deep in a program that cannot reasonably wind its way back to main. (And lesson 12.3 will give main itself a way to report failure without exit.)

panic!, formally introduced

You've been causing panics since chapter 3 (expect on bad input, overflow in debug builds, dividing by an input zero) and reading their reports fluently. What we haven't done is show you the macro they're all built on. panic! ends the program on purpose, with a message, and with the distinctive crash report you know:

fn main() {
    let day = 8;
    if day > 7 {
        panic!("day {day} is not a day of the week");
    }
    println!("scheduling day {day}");
}
thread 'main' (2298525) panicked at src/main.rs:4:9:
day 8 is not a day of the week
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
$ echo $?
101

A panic always exits with code 101 (Rust's convention for "this program crashed," as opposed to "this program reported failure"), and unlike exit, a panic performs an orderly retreat: it unwinds back through the unfinished calls, dropping values properly along the way.

So when exit and when panic!? By meaning. An exit code is a program's normal vocabulary for outcomes, including bad ones; "the file you asked for doesn't exist" is a Tuesday, not an emergency. A panic means the program itself is broken: some situation the author believed impossible has happened, and continuing would compute nonsense. panic!("day {day} is not a day of the week") says no correct caller can ever put us here; the message is addressed to the programmer who eventually does.

Chapter 12 owns the rest of this story, including the tool that handles expected failures without halting anything (Result, the reason well-built Rust programs panic so rarely), and the fuller decision rules. Until then, your expect calls panic on bad input, and for learning programs, that's fine.

For advanced readers

There's a third, rarer halt: std::process::abort() ends the program instantly with no unwinding, no message, and no cleanup at all, the kind of stop reserved for "this process can no longer be trusted to run its own shutdown code." The stack overflow in lesson 7.7 ends this way, which is why its report is two terse lines instead of a friendly panic message.

Quiz time

Question #1

A shell script runs your program and checks $?. Your program found nothing to do, did nothing, and ended normally. What exit code does the script see, and is "did nothing" a failure?

Show solution

0: main returned normally, so the program reports success. Whether "nothing to do" should count as success is a design decision (some tools, like grep, deliberately use a nonzero code for "no matches found"). The mechanism only reports what you tell it; if "nothing to do" is failure for your tool, that's a process::exit(1).

Question #2

What does this program print, and what's its exit code?

use std::process;

fn check(n: i32) {
    println!("checking {n}");
    if n < 0 {
        println!("negative, bailing out");
        process::exit(2);
    }
    println!("{n} is fine");
}

fn main() {
    check(5);
    check(-1);
    check(7);
}
Show solution
checking 5
5 is fine
checking -1
negative, bailing out

Exit code 2. The second call hits the exit, which ends the whole program from inside check: no return to main, so check(7) never happens. (That long reach is exactly why exit deserves suspicion; a reader of main alone can't see that the third call is unreachable.)

Question #3

Pick the right ending (return normally, process::exit with a nonzero code, or panic!) for each: (a) a file-conversion tool was given a file that doesn't exist; (b) a function's match arm is reached that the author proved unreachable; (c) your program finished everything it was asked to do.

Show solution

(a) process::exit with a nonzero code, after printing a helpful message: a missing file is an expected failure, part of the tool's normal vocabulary (and chapter 12 will upgrade this answer to Result). (b) panic!: the program's own logic has been violated; the message is for its author, and the crash report with its line number is the feature. (c) Return from main; exit code 0 happens on its own.

Next: your program leaves the standard library for the first time. Random numbers, and the package registry where the rest of Rust lives.