TL;DR: A panic in Rust is an unrecoverable error: it unwinds the call stack (running destructors), prints a message and backtrace, and terminates the current thread. Panics are for programming bugs, violated invariants, index out of bounds,
unwrap()onNone. They are not for expected error conditions like bad user input or missing files, useResult<T, E>for those. In web servers (Axum, Actix), panics in request handlers are caught at the thread/task boundary and converted to 500 responses, preventing one bad request from crashing the whole server.
What Is a Panic in Rust?
A panic signals that the program has reached an unrecoverable state, a bug, not a user error. Rust unwinds the stack, runs all destructors, and terminates the thread.
fn main() {
panic!("something went terribly wrong"); // explicit panic
let v = vec![1, 2, 3];
println!("{}", v[10]); // index out of bounds; panics
let x: Option<i32> = None;
let _ = x.unwrap(); // called unwrap() on None; panics
let s: &str = "abc";
let _ = s.parse::<i32>().unwrap(); // parse error; panics
}When a panic occurs, Rust prints:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 10', src/main.rs:5:20Set RUST_BACKTRACE=1 to get a full stack trace.
How Is Panic Different From Result?
Result is for expected, recoverable failures, call it "operational error." Panic is for bugs and violated invariants, call it "programmer error."
| Situation | Use |
|---|---|
| File not found | Result<_, io::Error> |
| Invalid user input | Result<_, ValidationError> |
| Database query failed | Result<_, sqlx::Error> |
| Network timeout | Result<_, reqwest::Error> |
| Index out of bounds | Panic, it's a bug |
| Division by zero | Panic (in debug), wraps in release |
unwrap() on None you guaranteed isn't None | Panic, if it panics, your guarantee was wrong |
| Impossible state reached | unreachable!(), panics with a clear message |
// WRONG; using panic for recoverable errors
fn read_config(path: &str) -> String {
std::fs::read_to_string(path).unwrap() // panics if file missing
}
// RIGHT; return Result, let caller decide
fn read_config(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}What Happens When a Panic Occurs?
By default, Rust unwinds the stack, running destructors for all values in scope, then terminates the thread. The abort profile option skips unwinding and kills immediately.
# Cargo.toml; use abort instead of unwind (smaller binary, no recovery)
[profile.release]
panic = "abort"In a multi-threaded program, a panic in one thread does not automatically kill other threads. thread::spawn returns a JoinHandle, calling .join() returns Err if the thread panicked.
use std::thread;
fn main() {
let handle = thread::spawn(|| {
panic!("oops");
});
match handle.join() {
Ok(_) => println!("thread finished normally"),
Err(_) => println!("thread panicked"), // caught here
}
}How Do Web Servers Handle Panics?
Axum and Actix-web catch panics at the request handler boundary and convert them to 500 responses, preventing one bad request from crashing the whole server.
use axum::{Router, routing::get};
async fn handler() -> &'static str {
panic!("something went wrong"); // caught by Axum's panic handler
"never reached"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(handler));
// A panic in handler() returns HTTP 500, server keeps running
}Axum uses tower's CatchPanic middleware under the hood. For production, install a panic hook (via std::panic::set_hook) to log panics to your monitoring system before they are swallowed.
Frequently Asked Questions
Rarely. .unwrap() is a panic waiting to happen. It is acceptable when you can prove None/Err is impossible (e.g., "42".parse::<i32>().unwrap() on a string literal), or in test code. For anything else, propagate with ?, handle with match, or use .unwrap_or().
.expect("message") is like .unwrap() but panics with a custom message. Prefer it over .unwrap(), the message explains why you believed this couldn't fail: .expect("config must be valid UTF-8"). When it does fail, you get an actionable error instead of a cryptic message.
Yes, with std::panic::catch_unwind(|| { ... }), it returns Err if the closure panicked. This is primarily for FFI boundaries and test frameworks, not for normal error handling. Do not use it as a substitute for Result.
unreachable!() is a macro that panics with the message "internal error: entered unreachable code." Use it to mark branches that should be logically impossible, if they are reached, it is a bug and the panic message makes that clear.
Sources
Related Glossary Terms
- Result: The right tool for recoverable errors instead of panic
- Option:
unwrap()onNoneis a common panic source - Ownership: Panics still run destructors, owned values are always cleaned up
- Async/Await: Panics in async tasks are caught per-task, not globally
