Skip to content

Rust Panic

A panic is Rust’s way of stopping a program immediately when something has gone unrecoverably wrong. You’ve actually already caused a few without meaning to, if you’ve followed along with earlier pages — indexing past the end of an array, or calling .unwrap() on a None, both panic.

fn main() {
panic!("something went badly wrong");
}
thread 'main' (12345) panicked at src/main.rs:2:5:
something went badly wrong
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The number in parentheses, (12345) here, is the thread’s ID. You’ll see a different number on every run, and older Rust versions leave it out.

▶ Try it Yourself

When this runs, the program prints that message and stops — nothing after the panic! executes.

Things that panic without you calling panic! directly

Section titled “Things that panic without you calling panic! directly”

A few operations you’ve already met will panic on their own if something’s wrong. Each example below reads its risky value through a function parameter rather than a hardcoded literal — with a literal index or a literal 0 right there in the code, the compiler can often work out at compile time that the operation always fails, and refuses to build at all instead of letting it run and panic.

fn get(numbers: &[i32], index: usize) -> i32 {
numbers[index] // panics: index out of bounds
}
fn main() {
let numbers = [1, 2, 3];
println!("{}", get(&numbers, 10));
}
▶ Try it Yourself
fn main() {
let value: Option<i32> = None;
println!("{}", value.unwrap()); // panics: called `Option::unwrap()` on a `None` value
}
▶ Try it Yourself
fn divide(a: i32, b: i32) -> i32 {
a / b // panics: attempt to divide by zero
}
fn main() {
let a = 10;
let b = 0;
println!("{}", divide(a, b));
}
▶ Try it Yourself

Each of these is Rust refusing to let your program continue in a state it can’t make sense of, rather than quietly returning garbage.

This is the real decision you’ll make often: should this failure crash the program, or should the caller get a chance to handle it?

  • panic! — for bugs and situations that should never happen if the code is correct. There’s no sensible way to recover, so stopping immediately (before doing more damage) is the safest option.
  • Result (covered on the previous page) — for failures a caller might reasonably expect and want to handle, like bad user input, a missing file, or a failed network request.
// Bad input from a user — recoverable, use Result
fn parse_age(input: &str) -> Result<u32, String> {
input.parse().map_err(|_| String::from("not a valid age"))
}
// An invariant that should be impossible if the code is correct — panic
fn get_first(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
panic!("get_first called with an empty slice");
}
numbers[0]
}

unwrap() and expect() are shortcuts to panicking

Section titled “unwrap() and expect() are shortcuts to panicking”

You’ve seen .unwrap() on both Option and Result — it’s really just a shortcut for “give me the value, or panic if there isn’t one.” .expect("message") does the same thing but lets you attach a more useful message:

fn main() {
let input = "42";
let number: i32 = input.parse().expect("input should be a valid number");
println!("{number}");
}
▶ Try it Yourself

If parsing fails, the panic message includes your text, which makes debugging a lot easier than the generic message .unwrap() gives you.

Checking your assumptions: assert!, assert_eq! and unreachable!

Section titled “Checking your assumptions: assert!, assert_eq! and unreachable!”

Sometimes you know something must be true at a certain point in your code. Rust has a few small macros that turn that belief into a check. If the check fails, they panic with a clear message, so a bug shows up right where it starts instead of causing strange results later.

  • assert!(condition, "message") panics if the condition is false.
  • assert_eq!(left, right) panics if the two values are not equal, and prints both of them.
  • unreachable!("message") marks code that should never run, like a match arm for a value that can’t happen.
fn average(numbers: &[i32]) -> i32 {
assert!(!numbers.is_empty(), "average needs at least one number");
let mut total = 0;
for n in numbers {
total += n;
}
total / numbers.len() as i32
}
fn day_name(day: u8) -> &'static str {
match day {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
6 => "Saturday",
7 => "Sunday",
_ => unreachable!("day must be 1 to 7, got {day}"),
}
}
fn main() {
let avg = average(&[70, 80, 90]);
assert_eq!(avg, 80);
println!("average: {avg}");
println!("day 3 is {}", day_name(3));
}

Output:

average: 80
day 3 is Wednesday
▶ Try it Yourself

Every check passes here, so the program runs normally. Now change the list to [70, 80, 95] in the playground. The average becomes 81, and assert_eq! stops the program with both values side by side:

thread 'main' (12345) panicked at src/main.rs:26:5:
assertion `left == right` failed
left: 81
right: 80

Try average(&[]) or day_name(9) too, and you’ll see your own messages in the panic.

These checks always run, even in a release build, so keep them cheap. You’ll meet assert_eq! again on the Testing page, where it’s the main way a test says “this should equal that”. There is also todo!(), which panics with “not yet implemented”. It’s handy as a placeholder body while you sketch out a function that you’ll finish later.

You have a config value let port_text = "8080";. Turn it into a u16 with .parse() and .expect(...), using a message that explains what went wrong. Then print Listening on port 8080. Hint: the variable needs a type, let port: u16 = ..., so parse knows what to produce. Afterwards, change the text to "80a0" to see your message in the panic.

Show solution
fn main() {
let port_text = "8080";
let port: u16 = port_text
.parse()
.expect("the port in the config should be a number from 0 to 65535");
println!("Listening on port {port}");
}

Output:

Listening on port 8080
▶ Try it Yourself

Write fn split_bill(total: u32, people: u32) -> u32 that returns each person’s share. Splitting between zero people is a bug in the calling code, so use assert! with a clear message to catch it. Print the share for a total of 120 and 4 people. Hint: put the assert! on the first line of the function, before the division.

Show solution
fn split_bill(total: u32, people: u32) -> u32 {
assert!(people > 0, "split_bill needs at least one person");
total / people
}
fn main() {
let share = split_bill(120, 4);
println!("Each person pays {share}");
}

Output:

Each person pays 30
▶ Try it Yourself
  • panic! stops the program immediately with an error message.
  • Some standard operations (bad array index, .unwrap() on None, division by zero) panic automatically.
  • Use panic! for bugs/invariants that should be impossible; use Result for failures a caller can reasonably handle.
  • .expect("message") is like .unwrap(), but with a message that makes the panic easier to debug.
  • assert!, assert_eq! and unreachable! panic on purpose when something you believed to be true turns out false.

Quick check

1. What does calling panic! do?

2. According to the rule of thumb on this page, when should you use panic! instead of Result?

3. What's the difference between .unwrap() and .expect('message')?

Score: 0 / 3