---
title: "7.8 Halting your program early"
description: "Exit codes, returning from main, std::process::exit, and a first proper introduction to panic!."
url: "https://learnrust.net/chapter-7/halting-your-program-early/"
last_updated: "2026-06-12"
---

# 7.8 Halting your program early

Lesson [2.2](https://learnrust.net/chapter-2/return-values/) 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:

```bash
$ 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.

```rust
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
```

```bash
$ 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:

```rust
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
```

```bash
$ 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](https://learnrust.net/chapter-7/introduction-to-recursion/) 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?

<details class="solution">
<summary>Show solution</summary>

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)`.

</details>

**Question #2**

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

```rust
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);
}
```

<details class="solution">
<summary>Show solution</summary>

```
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.)

</details>

**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.

<details class="solution">
<summary>Show solution</summary>

(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.

</details>

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

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
