What Is the Difference Between panic! and Result?

Use panic! to crash on unrecoverable bugs and Result to handle expected errors gracefully.

When the program can't continue versus when it can

You are building a text-based adventure game. The player types open door. Your code looks up the door ID in the game map. The ID is missing. That is a bug in your game logic. The map is corrupted. The game cannot possibly continue in a valid state. You crash the program.

Now imagine the player types open fridge. The fridge is not in the current room. That is not a bug. The player made a mistake. You tell them "The fridge isn't here" and let them try again. The game keeps running.

One situation destroys the program. The other keeps it alive. Rust forces you to choose which is which. panic! is for the missing door ID. Result is for the missing fridge.

The fire alarm versus the sign

panic! is the fire alarm. When it goes off, everyone stops, drops everything, and leaves. The show is over. Result is the "Out of Order" sign on a bathroom door. People see it, adjust their plans, and keep moving through the building. The show goes on.

In Rust, panic! means something went wrong so badly the program cannot possibly continue safely. Result means something went wrong, but the program can handle it and keep going. The distinction is not about severity. It is about recoverability. If the caller can do anything useful with the error, you must use Result. If the caller can do nothing, you use panic!.

Minimal examples

/// Crashes the program immediately. No return value.
fn crash() {
    // This function never returns.
    panic!("Something broke beyond repair");
}

/// Returns control to the caller with a success or failure value.
fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        // Return an error variant. The caller decides what to do.
        Err("Cannot divide by zero".to_string())
    } else {
        // Return a success variant.
        Ok(a / b)
    }
}

panic! is a macro that takes a message. It stops execution right there. Result is an enum with two variants: Ok(T) for success and Err(E) for failure. Functions return Result to hand the error back to the caller. The caller receives the value and chooses how to proceed.

What happens under the hood

When panic! runs, Rust starts unwinding the stack. It walks backward through the call stack, calling destructors for every value on the way. This ensures memory is freed and resources are closed. Unwinding is safe but has a cost. It takes time and requires extra metadata in the binary.

Rust offers a second panic strategy: abort. You can set panic = "abort" in Cargo.toml. In abort mode, panic! kills the process immediately. No unwinding. No destructors. This is faster and produces smaller binaries. Use abort for final release builds where size or speed matters more than graceful cleanup. Libraries should never rely on abort mode because the application might be using unwinding.

With Result, nothing special happens at runtime. It is just a value. The function returns Ok(value) or Err(error). The caller receives this value. The caller can match on it, unwrap it, or pass it up to their caller. The program stays alive. The compiler enforces this discipline. If a function returns Result, the caller must handle both cases. If you ignore the result, the compiler warns you. If you try to use the value without handling the error, the compiler rejects you.

fn process() -> Result<(), std::io::Error> {
    // The ? operator checks the Result.
    // If Ok, it extracts the value.
    // If Err, it returns the error from this function immediately.
    let data = std::fs::read_to_string("data.txt")?;
    
    println!("{}", data);
    Ok(())
}

The ? operator is the engine of Result handling. It turns nested matches into a flat chain. It relies on the From trait to convert error types. If you return Result<T, MyError> and you use ? on a Result<T, IoError>, Rust tries to convert IoError to MyError. If the conversion exists, it works. If not, you get a type error. This is why custom error types often implement From for common errors like std::io::Error.

Convention aside: error messages should go to stderr, not stdout. Use eprintln! for errors. This keeps error output separate from program output, which matters when users pipe results to other tools.

Realistic scenario: loading configuration

You are writing a CLI tool. It needs a configuration file. If the file is missing, that is a user error. You want to print a nice message and exit. If the JSON is malformed, that is also a user error. You handle both with Result.

use std::fs;
use std::io;

/// Reads the config file and returns the content or an I/O error.
fn load_config(path: &str) -> Result<String, io::Error> {
    // fs::read_to_string returns a Result.
    // We return it directly to propagate the error.
    fs::read_to_string(path)
}

fn main() {
    // Handle the Result in main.
    match load_config("config.json") {
        Ok(content) => println!("Loaded config: {}", content),
        Err(e) => {
            // User error. Print to stderr and exit with non-zero code.
            eprintln!("Error loading config: {}", e);
            std::process::exit(1);
        }
    }
}

This pattern is standard for CLI tools. The library function returns Result. The main function handles the error and decides to exit. The library does not panic. The application panics or exits based on its needs.

Convention aside: when you must panic, use .expect("message") instead of .unwrap(). unwrap() panics with a generic message. expect() includes your message in the panic output. expect() tells the reader why the panic is expected to succeed. unwrap() looks lazy. The community treats unwrap() as a code smell in production code.

Pitfalls and compiler errors

Panic in a library is a design mistake. If you write a library and you panic, you crash the user's program. The user cannot recover. The user cannot even catch the panic easily. Return a Result instead. Let the application decide to panic. Libraries should be resilient. Applications can be fragile.

If your function signature says -> Result<T, E> but you return a T directly, the compiler rejects you with E0308 (mismatched types). You have to wrap the value in Ok(t). This error catches a common mistake where developers forget the wrapper.

fn bad() -> Result<i32, String> {
    // E0308: mismatched types.
    // Expected Result<i32, String>, found i32.
    42
}

fn good() -> Result<i32, String> {
    // Wrap the value in Ok.
    Ok(42)
}

Tests are a special case. In tests, you want panics. If an assertion fails, the test should crash. That is how you know it failed. assert_eq! panics on mismatch. This is intentional. The test harness catches the panic and marks the test as failed. So panics are good in tests. They are bad in production libraries.

Let the application crash, not the library.

When to use panic! versus Result

Use panic! when the program is in an invalid state and cannot continue. Use panic! for unrecoverable bugs like out-of-bounds access in safe code or assertion failures. Use panic! in main when a required resource is missing and the program has no purpose without it. Use panic! in tests to signal assertion failures.

Use Result when the error is expected and recoverable. Use Result for I/O operations like file reads or network requests. Use Result for parsing user input where bad input is normal. Use Result in libraries so callers can decide how to handle failures. Use Result when the caller might want to retry the operation.

Reach for Option when the absence of a value is a normal case, not an error. Reach for unwrap_or when you have a sensible default and do not need to distinguish success from failure. Reach for expect when you must panic but want to provide context.

If you can recover, you must. If you can't, crash loud and early.

Where to go next