Skip to content
Educora
Intermediate18 min6 / 10

Error handling: panic, Result and ?

Tell unrecoverable errors (`panic!`) from recoverable ones (`Result`), understand the risks of `unwrap` and `expect`, write short code with the `?` operator and create your own error type.

Check yourself
In this lesson you will learn
  • Choose correctly between a panic and a Result
  • Handle a Result with match, unwrap_or and expect
  • Pass an error up to the caller with the ? operator
  • Create your own error enum that implements Display

You ask the user for their age and they type abc. You want to read a file, but the file does not exist. These are not bugs in your program but expected situations — the program should respond to them without crashing. Rust asks you to show every possible failure in the types, and the compiler does not let you forget them.

Two kinds of errors

An unrecoverable error means there is a bug in the program: going past the end of an array, or a situation that “should never happen”. In that case Rust panics: it prints an error message and stops the thread. You can also call panic!("message") yourself. Recoverable errors, on the other hand, are returned with the Result<T, E> type and handled by the calling code.

Rust
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("division by zero: {a} / {b}");
    }
    a / b
}

fn main() {
    println!("{}", divide(10, 2));
    println!("{}", divide(1, 0));
    println!("never printed");
}
Expected output
5
thread 'main' panicked at src/main.rs:3:9:
division by zero: 1 / 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The panic message goes to the standard error stream (stderr) and shows the file, line and column; the last line is never reached. Going past the end of an array produces a similar message: index out of bounds: the len is 3 but the index is 10.

The Result type

Result<T, E> is an enum with two variants: Ok(T) on success and Err(E) on failure. For example, the parse method, which turns text into a number, returns a Result, because the text may not be a number. With match we handle both cases separately:

Rust
fn main() {
    let inputs = ["42", "abc", ""];
    for input in inputs {
        match input.parse::<i32>() {
            Ok(n) => println!("{input:?} -> {}", n * 2),
            Err(e) => println!("{input:?} -> error: {e}"),
        }
    }
}
Expected output
"42" -> 84
"abc" -> error: invalid digit found in string
"" -> error: cannot parse integer from empty string
In parse::<i32>(), the ::<i32> part says which type to convert to (people call it the “turbofish”). The error is printed with {e} as human-readable text.

unwrap, expect and other shortcuts

Rust
fn main() {
    let a: i32 = "7".parse().unwrap();
    let b: i32 = "8".parse().expect("b must be a number");
    let c: i32 = "x".parse().unwrap_or(0);
    let d: i32 = "".parse().unwrap_or_default();
    println!("{}", a + b + c + d);
}
Expected output
15
Parsing failed for c and d, but the program did not stop: unwrap_or(0) and unwrap_or_default() returned a fallback value (0). 7 + 8 + 0 + 0 = 15.
MethodOn ErrWhen to use
matchyour own code for each casewhen you need full control
?returns the error to the callerwhen the function itself returns Result
unwrap_or(v)returns vwhen a sensible fallback exists
expect("...")panics with your messagewhen failure means a bug
unwrap()panicsonly in examples and tests

The ? operator

? is written after a Result. If the result is Ok, ? takes out the value and the code continues. If it is Err, the current function returns that error immediately. This way errors are passed up to the caller without writing a match at every step. The condition: the function itself must return a Result (or an Option).

Rust
use std::num::ParseIntError;

fn sum_pair(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x: i32 = a.trim().parse()?;
    let y: i32 = b.trim().parse()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", sum_pair("10", " 32 "));
    println!("{:?}", sum_pair("10", "ten"));
    match sum_pair("5", "") {
        Ok(total) => println!("total = {total}"),
        Err(e) => println!("failed: {e}"),
    }
}
Expected output
Ok(42)
Err(ParseIntError { kind: InvalidDigit })
failed: cannot parse integer from empty string
trim() removes spaces at the edges. {:?} shows the error's inner structure, while {e} shows its readable message.
Without ?
use std::num::ParseIntError;

fn read_age(s: &str) -> Result<u8, ParseIntError> {
    let age = match s.parse::<u8>() {
        Ok(value) => value,
        Err(e) => return Err(e),
    };
    Ok(age)
}
With ?
use std::num::ParseIntError;

fn read_age(s: &str) -> Result<u8, ParseIntError> {
    let age = s.parse::<u8>()?;
    Ok(age)
}
Both functions do the same job. When cargo clippy sees the version on the left, it suggests replacing it with ? (the question_mark lint).
Error E0277
fn main() {
    let n: i32 = "5".parse()?;
    println!("{n}");
}
Correct: prints `5`
use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError> {
    let n: i32 = "5".parse()?;
    println!("{n}");
    Ok(())
}
A plain main returns nothing, so the compiler says: the ? operator can only be used in a function that returns Result or Option. main can return a Result too; on Err the error is printed and the program exits with a failure code.

Your own error type

In a large program, errors deserve their own names. Write the kinds of errors as an enum and implement the Display trait for it, so the error is shown to the user as a readable message. The map_err method turns one error type into another — it is often used together with ?. (You will study traits in detail in lesson 8.)

Rust
use std::fmt;

#[derive(Debug)]
enum AgeError {
    NotANumber,
    TooOld(u32),
}

impl fmt::Display for AgeError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AgeError::NotANumber => write!(f, "age must be a number"),
            AgeError::TooOld(n) => write!(f, "{n} is too old"),
        }
    }
}

fn parse_age(s: &str) -> Result<u32, AgeError> {
    let age: u32 = s.parse().map_err(|_| AgeError::NotANumber)?;
    if age > 150 {
        return Err(AgeError::TooOld(age));
    }
    Ok(age)
}

fn main() {
    for s in ["15", "abc", "200"] {
        match parse_age(s) {
            Ok(age) => println!("ok: {age}"),
            Err(e) => println!("error: {e} / {e:?}"),
        }
    }
}
Expected output
ok: 15
error: age must be a number / NotANumber
error: 200 is too old / TooOld(200)
{e} uses our Display implementation, and {e:?} uses the text generated by #[derive(Debug)]. |_| is a short function that ignores its parameter (a closure, lesson 7).

Key points

  • panic! stops the program on a bug; expected errors are returned as Result<T, E>.
  • A Result holds either Ok(value) or Err(error) and is handled with match.
  • unwrap and expect panic on Err; in production code prefer ?, match or unwrap_or.
  • ? unwraps Ok and returns Err to the caller at once; it only works in functions that return Result or Option.
  • For your own error type, write an enum, implement Display for it and convert errors with map_err.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
What does the ? operator do when the result is Err?