| |

Rust 32 🦀 Unrecoverable Errors with panic!

Not every error should be handled. Some errors indicate that the program has reached a state it cannot reason about, and continuing would produce incorrect results or corrupt data. For these, Rust provides panic!. A panic prints a message, unwinds the stack, and terminates the thread. It is the mechanism for the unrecoverable error, and it is the counterpart to Result, which is the mechanism for the recoverable one. Knowing when to panic and when to return a Result is one of the recurring decisions in Rust.

This chapter covers panic! in full. You will learn what a panic does, the difference between unwinding and aborting, the macros and methods that panic, the ways to catch a panic, and the guidelines for choosing between panic! and Result. You will also see the panic! message, the backtrace, and the difference between a panic in the main thread and a panic in a spawned thread.

Key point: A panic is for unrecoverable errors: bugs, violated invariants, and conditions the program cannot recover from. A Result is for recoverable errors: a missing file, a network failure, invalid input. The panic! macro terminates the thread; the Result propagates to the caller, who decides what to do. The unwrap and expect methods panic on None or Err, and they are the shorthand for “this should never happen.”


Why panic! exists

The invariant problem. A function may have a precondition that, if violated, makes its behavior undefined. An index into a slice must be within bounds. A division must not have a zero denominator. A state machine must be in a valid state. When the precondition is violated, the function cannot produce a correct result. The panic is the signal that the invariant is broken and the program should not continue.

The bug problem. A panic often indicates a bug in the program. The unwrap on an Option that is None when it should be Some, the expect on a Result that is Err when it should be Ok, the array index that is out of bounds. These are the cases where the program’s assumptions are wrong, and the panic surfaces the assumption.

The recoverable problem. Not every error is a bug. A file that does not exist, a network that is down, a user who enters invalid input—these are conditions the program can handle. The Result type is for these. The panic! is for the conditions the program cannot handle.

The boundary problem. A library function does not know how the caller wants to handle an error. A panic removes the choice. A Result gives the choice to the caller. The convention is that a library returns Result for the errors the caller might want to handle, and panics only for the errors that indicate a bug in the library’s usage or a broken invariant.

The performance problem. A Result has a small cost: the Ok and Err variants, the check at the call site. A panic is the path that is not taken in the common case. The panic is the cold path, and the Result is the warm path. The choice between them is the choice between handling and not handling.

The thread problem. A panic in a thread does not necessarily terminate the program. The thread unwinds and terminates, and the program continues if the panic is caught or if the thread is not the main thread. The isolation is the mechanism for the programs that use multiple threads and want one thread’s panic to not bring down the others.


a. The panic! macro and the panic methods

The panic! macro terminates the thread with a message.

fn main() {
    panic!("Something went wrong");
}

The output is:

thread 'main' panicked at src/main.rs:2:5:
Something went wrong
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The message includes the file and line, the panic message, and a note about the backtrace. The RUST_BACKTRACE=1 environment variable enables the backtrace.

The panic! macro can format the message.

let x = 42;
panic!("The value is {}", x);

The format string and the arguments are the same as println!.

The panic! can be called with no arguments.

panic!();

The message is empty, and the output shows only the location.

The unwrap method panics if the value is None or Err.

let x: Option<i32> = None;
x.unwrap(); // panics with "called `Option::unwrap()` on a `None` value"

The unwrap on an Option panics with the message “called Option::unwrap() on a None value”. The unwrap on a Result panics with the message “called Result::unwrap() on an Err value: “.

The expect method is the same as unwrap with a custom message.

let x: Option<i32> = None;
x.expect("x should be Some"); // panics with "x should be Some"

The expect is the preferred form when the panic message should explain the assumption.

The unwrap and expect are the shorthands for the “should never happen” cases. They are appropriate when the program’s logic guarantees the value is present, and a None or Err would indicate a bug.

let config = std::env::var("CONFIG").expect("CONFIG must be set");

The expect documents the assumption. If the assumption is wrong, the panic message says which one.

The indexing of a slice or array panics if the index is out of bounds.

let v = vec![1, 2, 3];
let x = v[10]; // panics with "index out of bounds"

The get method is the non-panicking alternative.

let v = vec![1, 2, 3];
let x = v.get(10); // None

The get returns Option<&T>, and the caller handles the None.

The integer division by zero panics.

let x = 10 / 0; // panics with "attempt to divide by zero"

The division by zero is the runtime check. The checked_div method is the non-panicking alternative.

let x = 10.checked_div(0); // None

The checked_div returns Option<i32>, and the caller handles the None.


b. Unwinding and aborting

A panic has two possible behaviors: unwinding and aborting. The default is unwinding.

Unwinding means the stack is unwound, the destructors are run, and the thread is terminated. The local variables are dropped in reverse order of their declaration. The memory is cleaned up.

struct Resource;
impl Drop for Resource {
    fn drop(&mut self) {
        println!("Resource dropped");
    }
}

fn main() {
    let _r = Resource;
    panic!("Something went wrong");
}

The output is:

Resource dropped
thread 'main' panicked at ...

The destructor runs before the panic message. The unwinding runs the destructors.

Aborting means the process is terminated immediately. The destructors are not run, and the memory is reclaimed by the operating system. The abort is faster, but the cleanup is skipped.

The behavior is configured in Cargo.toml.

[profile.release]
panic = "abort"

The panic = "abort" in the release profile makes the release build abort on panic. The debug build unwinds by default. The abort is used when the binary size matters or when the cleanup is not needed because the process is ending.

The catch_unwind function catches a panic and returns a Result.

use std::panic;

let result = panic::catch_unwind(|| {
    panic!("Something went wrong");
});

match result {
    Ok(_) => println!("No panic"),
    Err(_) => println!("Panic caught"),
}

The catch_unwind runs the closure and catches the panic. The result is Ok if no panic, Err if a panic. The panic message is in the Err payload.

The catch_unwind is used for the cases where the panic is expected and the program wants to continue. It is not the general error-handling mechanism; it is the mechanism for the libraries that need to isolate the panics.

The catch_unwind requires the closure to be UnwindSafe.

use std::panic::{catch_unwind, UnwindSafe};

fn main() {
    let x = 42;
    let result = catch_unwind(|| {
        if x > 0 {
            panic!("x is positive");
        }
    });
}

The UnwindSafe is a marker trait that indicates the value can be safely used after a panic. The mutable references and the RefCell are not UnwindSafe by default because a panic could leave them in an inconsistent state.

The AssertUnwindSafe wrapper bypasses the UnwindSafe check.

use std::panic::{catch_unwind, AssertUnwindSafe};

let mut x = 0;
let result = catch_unwind(AssertUnwindSafe(|| {
    x += 1;
    panic!("Something went wrong");
}));

The AssertUnwindSafe tells the compiler that the value is safe to use after the panic. The developer is responsible for the safety.

A panic in a spawned thread does not terminate the program.

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        panic!("Thread panicked");
    });

    let result = handle.join();
    match result {
        Ok(_) => println!("Thread completed"),
        Err(_) => println!("Thread panicked"),
    }
    println!("Main continues");
}

The output is:

thread '<unnamed>' panicked at ...
Thread panicked
Main continues

The thread panics, the join returns Err, and the main thread continues. The panic is isolated to the spawned thread.

The join method returns Result<T, Box<dyn Any + Send>>. The Err variant contains the panic payload, which can be downcast to the original message.

let result = handle.join();
if let Err(e) = result {
    if let Some(msg) = e.downcast_ref::<&str>() {
        println!("Panic message: {}", msg);
    }
}

The payload is a Box<dyn Any + Send>, and the downcast_ref recovers the original message if it is a &str or String.


c. When to panic and when to return Result

The choice between panic! and Result is the choice between the unrecoverable and the recoverable. The guidelines:

SituationUse
A bug in the programpanic!
A broken invariantpanic!
A precondition violatedpanic!
A recoverable errorResult
A user input errorResult
A network failureResult
A file not foundResult
A library functionResult (usually)

The panic! is for the errors the program cannot handle. The Result is for the errors it can.

A library function that panics on the caller’s input is the library making a decision for the caller. The convention is that the library returns Result and lets the caller decide.

// Library: returns Result
pub fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse::<i32>()
}

// Caller: decides to panic or handle
let n = parse_number("42").expect("Invalid number");

The parse_number returns Result. The caller uses expect to panic on the invalid input, or matches to handle it. The library does not decide.

A function that panics on an internal invariant is the function signaling a bug. The panic is the correct choice because the caller cannot fix the bug.

fn get_first(v: &[i32]) -> i32 {
    if v.is_empty() {
        panic!("get_first called on an empty slice");
    }
    v[0]
}

The get_first panics if the slice is empty. The caller’s contract is to pass a non-empty slice. If the caller violates the contract, the panic surfaces the violation.

The assert! and assert_eq! macros panic if the condition is false.

fn divide(a: i32, b: i32) -> i32 {
    assert!(b != 0, "b must not be zero");
    a / b
}

The assert! checks the precondition. The assert_eq! and assert_ne! check the equality.

assert_eq!(2 + 2, 4);
assert_ne!(2 + 2, 5);

The assertion macros are for the invariants that should hold. They are the debug-time check that surfaces the bugs.

The debug_assert! is the same as assert! but is compiled out in the release build.

debug_assert!(b != 0, "b must not be zero");

The debug_assert! is for the expensive checks that are not needed in production. The assert! is for the checks that are cheap and should always run.

The todo! and unimplemented! macros panic with a message that the code is not implemented.

fn not_yet() -> i32 {
    todo!("Implement this later")
}

fn never_called() -> i32 {
    unimplemented!("This function is not implemented")
}

The todo! is for the code that will be implemented. The unimplemented! is for the code that will not be. Both panic.

The unreachable! macro panics with a message that the code should not be reached.

match value {
    1 => "one",
    2 => "two",
    _ => unreachable!("value must be 1 or 2"),
}

The unreachable! is for the branches that the logic guarantees cannot be reached. If they are, the panic surfaces the bug.


Complete Example Session

// ============================================
// PART 1: BASIC PANIC
// ============================================
fn main() {
    panic!("Something went wrong");
}
// ============================================
// PART 2: PANIC WITH FORMAT
// ============================================
let x = 42;
panic!("The value is {}", x);
// ============================================
// PART 3: UNWRAP
// ============================================
let x: Option<i32> = None;
x.unwrap(); // panics
// ============================================
// PART 4: EXPECT
// ============================================
let x: Option<i32> = None;
x.expect("x should be Some"); // panics with custom message
// ============================================
// PART 5: INDEX OUT OF BOUNDS
// ============================================
let v = vec![1, 2, 3];
let x = v[10]; // panics
// ============================================
// PART 6: DIVISION BY ZERO
// ============================================
let x = 10 / 0; // panics
// ============================================
// PART 7: ASSERT
// ============================================
fn divide(a: i32, b: i32) -> i32 {
    assert!(b != 0, "b must not be zero");
    a / b
}
// ============================================
// PART 8: CATCH_UNWIND
// ============================================
use std::panic;

let result = panic::catch_unwind(|| {
    panic!("Something went wrong");
});

match result {
    Ok(_) => println!("No panic"),
    Err(_) => println!("Panic caught"),
}
// ============================================
// PART 9: THREAD PANIC
// ============================================
use std::thread;

let handle = thread::spawn(|| {
    panic!("Thread panicked");
});

let result = handle.join();
println!("Main continues");
// ============================================
// PART 10: TODO AND UNREACHABLE
// ============================================
fn not_yet() -> i32 {
    todo!("Implement this later")
}

fn classify(n: i32) -> &'static str {
    match n {
        0 => "zero",
        1 => "one",
        _ => unreachable!("n must be 0 or 1"),
    }
}

The ten parts covered the basic panic, the panic with a format, unwrap, expect, the index out of bounds, the division by zero, assert!, catch_unwind, the thread panic, and todo! and unreachable!.


Quick Reference

Panic Macros

MacroPurpose
panic!(msg)Panic with a message
assert!(cond, msg)Panic if condition is false
assert_eq!(a, b)Panic if not equal
assert_ne!(a, b)Panic if equal
debug_assert!(cond)Panic in debug only
todo!(msg)Panic, not implemented
unimplemented!(msg)Panic, never implemented
unreachable!(msg)Panic, should not be reached

Panic Methods

MethodPanics On
unwrap()None or Err
expect(msg)None or Err
v[i]Out-of-bounds index
a / 0Division by zero

Panic vs Result

SituationUse
Bugpanic!
Invariantpanic!
Preconditionpanic!
RecoverableResult
LibraryResult
User inputResult

Panic Behavior

BehaviorDefaultConfig
UnwindingDebug—
AbortingRelease (opt-in)panic = "abort"
Catchcatch_unwind—
ThreadIsolatedjoin

Assertion Macros

MacroCheck
assert!Boolean condition
assert_eq!Equality
assert_ne!Inequality
debug_assert!Debug only

Best Practices

✅ Do This:

// Use panic! for bugs and invariants
assert!(b != 0, "b must not be zero");                         // ✅

// Use expect with a descriptive message
let config = env::var("CONFIG").expect("CONFIG must be set");  // ✅

// Use Result for recoverable errors
fn parse(s: &str) -> Result<i32, ParseIntError> { }            // ✅

// Use unreachable! for the impossible branches
_ => unreachable!("value must be 1 or 2"),                      // ✅

// Use todo! for the code to implement
todo!("Implement this later")                                  // ✅

// Use catch_unwind to isolate the panics
let result = catch_unwind(|| { });                             // ✅

// Use a thread to isolate the panic
let handle = thread::spawn(|| { panic!() });                   // ✅

❌ Don’t Do This:

// Don't panic on the user's input in a library
fn parse(s: &str) -> i32 {
    s.parse().unwrap() // panics on invalid input                // ❌
}

// Don't use unwrap when the value might be None
let value = map.get("key").unwrap();                           // ⚠️

// Don't use panic! for the recoverable errors
if file.is_none() {
    panic!("File not found"); // use Result instead              // ❌
}

// Don't catch_unwind for the normal error handling
let result = catch_unwind(|| { file.read() });                 // ⚠️

// Don't use expect without a message
let x = value.expect("");                                       // ⚠️

// Don't panic in Drop
impl Drop for Foo {
    fn drop(&mut self) { panic!("in drop"); }                  // ❌
}

// Don't rely on the panic message for the control flow
catch_unwind(|| { if x { panic!() } });                        // ❌

Common Pitfalls

PitfallWhy It HappensFix
Library panicsunwrap on caller inputReturn Result
Panic in DropDestructor panicsAvoid, abort instead
Panic swallowedcatch_unwind used for controlUse Result
Abort in releasepanic = "abort"Destructors skipped
Thread panic ignoredNo joinCheck the join result
Unwrap on NoneValue not guaranteedUse match or ?
unreachable! reachedLogic wrongFix the logic

Real-World Examples

1. Basic Panic

panic!("Something went wrong");

2. Assert

assert!(b != 0, "b must not be zero");

3. Expect

let config = env::var("CONFIG").expect("CONFIG must be set");

4. Unwrap on Option

let value = option.unwrap();

5. Unwrap on Result

let value = result.unwrap();

6. Unreachable

match value {
    0 => "zero",
    1 => "one",
    _ => unreachable!(),
}

7. Todo

fn not_yet() -> i32 {
    todo!("Implement this later")
}

8. Catch Unwind

let result = panic::catch_unwind(|| { panic!() });

9. Thread Panic

let handle = thread::spawn(|| panic!("Thread"));
let _ = handle.join();

10. Abort Configuration

[profile.release]
panic = "abort"

Visual

Panic Flow

┌─────────────────────────────────────────────────────────────┐
│  panic!("message")                                          │
│    │                                                        │
│    ▼                                                        │
│  Unwind the stack                                           │
│    │                                                        │
│    ├── Run the destructors                                  │
│    │                                                        │
│    ▼                                                        │
│  Print the message and location                             │
│    │                                                        │
│    ▼                                                        │
│  Terminate the thread                                       │
│    │                                                        │
│    ├── Main thread ──▶ Program exits                        │
│    │                                                        │
│    └── Spawned thread ──▶ Join returns Err                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Unwind vs Abort

┌─────────────────────────────────────────────────────────────┐
│  UNWIND (default)                                           │
│                                                             │
│  panic!()                                                   │
│    │                                                        │
│    ▼                                                        │
│  Run destructors                                            │
│    │                                                        │
│    ▼                                                        │
│  Terminate thread                                           │
│                                                             │
│  Cleanup happens. Slower.                                   │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ABORT (panic = "abort")                                    │
│                                                             │
│  panic!()                                                   │
│    │                                                        │
│    ▼                                                        │
│  Terminate process immediately                              │
│                                                             │
│  No cleanup. Faster. Smaller binary.                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Panic vs Result

┌─────────────────────────────────────────────────────────────┐
│  PANIC                                                      │
│                                                             │
│  Bug, invariant, precondition                               │
│  The program cannot continue                                │
│  The caller cannot fix it                                   │
│                                                             │
│  panic!("Index out of bounds")                              │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  RESULT                                                     │
│                                                             │
│  Recoverable error                                          │
│  The caller can decide                                      │
│  The program continues with a fallback                      │
│                                                             │
│  fn read() -> Result<String, io::Error>                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Thread Panic Isolation

┌─────────────────────────────────────────────────────────────┐
│  MAIN THREAD                                                │
│    │                                                        │
│    ├── spawn thread ──▶ THREAD                              │
│    │                      │                                 │
│    │                      ▼                                 │
│    │                    panic!()                             │
│    │                      │                                 │
│    │                      ▼                                 │
│    │                    thread terminates                    │
│    │                                                        │
│    ├── join() ──▶ Err(payload)                              │
│    │                                                        │
│    ▼                                                        │
│  Main continues                                             │
│                                                             │
│  The panic in the thread does not terminate the program.    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Macropanic!(msg)
Assertassert!(cond, msg)
Equalityassert_eq!(a, b)
Debug assertdebug_assert!(cond)
Unwrapx.unwrap()
Expectx.expect(msg)
Unreachableunreachable!()
Todotodo!()
UnwindDefault, runs destructors
Abortpanic = "abort", no destructors
Catchcatch_unwind
ThreadIsolated, join returns Err

Key takeaways:

  • panic! is for unrecoverable errors. The program has reached a state it cannot handle: a bug, a broken invariant, a violated precondition. The panic terminates the thread and prints a message.
  • Result is for recoverable errors. A file that does not exist, a network failure, invalid input—the caller can decide what to do. The library returns Result and lets the caller choose.
  • unwrap and expect panic on None or Err. They are the shorthand for “this should never happen.” The expect provides a message that documents the assumption.
  • The default behavior is unwinding. The stack is unwound, the destructors run, and the thread terminates. The panic = "abort" configuration in Cargo.toml aborts instead, skipping the destructors for a faster and smaller binary.
  • catch_unwind catches a panic. It is used to isolate the panics, not as the general error-handling mechanism. The closure must be UnwindSafe, and the AssertUnwindSafe bypasses the check.
  • A panic in a spawned thread does not terminate the program. The thread unwinds and terminates, and the join returns Err. The main thread continues.
  • The assertion macros check the invariants. assert!, assert_eq!, and assert_ne! panic if the condition is false. The debug_assert! is compiled out in the release build.

Remember: The panic! is the mechanism for the errors the program cannot recover from. Use it for the bugs and the broken invariants. Use Result for the errors the caller can handle. Use expect with a message for the assumptions. Use the assert! macros for the preconditions. And remember that the panic is the cold path: the Result is the warm path that the program uses, and the panic is the path that surfaces the bug when the program’s assumptions are wrong.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!