Free Handbook · Every example compiled & verified

Error Handling

panic! vs Result, unwrap and expect, match on errors, the ? operator, Option to Result, custom error types, Box<dyn Error> and main returning Result.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 06 · what you'll be able to do

  • Decide when a failure should panic and when it should return a Result
  • Handle a Result with match, unwrap_or, map_err and friends, and know when unwrap and expect are acceptable
  • Propagate errors with the ? operator and read the E0277 you get when the function cannot return one
  • Convert between Option and Result with ok_or and ok()
  • Write a custom error enum with Display, std::error::Error and From, or use Box when you just need any error
01

Two kinds of failure: panic! and Result

Rust has no exceptions. It splits failures into two kinds and gives each its own tool:

Unrecoverable: panic!

  • A bug: something that should be impossible happened
  • Out-of-bounds index, broken invariant, "this can never be None"
  • Stops the current thread with a message
  • You do not handle it; you fix the code

Recoverable: Result<T, E>

  • An expected failure: the file is missing, the input is not a number
  • The function returns Ok(value) or Err(error)
  • Part of the function's type, so callers cannot ignore it
  • The caller decides: retry, default, report, or pass it up

Result is an ordinary enum from the standard library, just like Option from Module 04: enum Result<T, E> { Ok(T), Err(E) }. Because the error is part of the return type, a function signature tells you exactly what can go wrong.

rustmain.rs
fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err(String::from("division by zero"));
    }
    Ok(a / b)
}

fn main() {
    let results = [divide(10, 2), divide(1, 0)];
    for r in &results {
        match r {
            Ok(v) => println!("ok: {v}"),
            Err(e) => println!("error: {e}"),
        }
    }
    println!("{:?}", divide(9, 3));
    println!("{:?}", divide(9, 0));
}
Outputcompiled & run with real Rust
ok: 5
error: division by zero
Ok(3)
Err("division by zero")
Error you will hit

Runtime panic: panic! stops the program

rust
fn percent(part: u32, whole: u32) -> u32 {
    if whole == 0 {
        panic!("percent() called with whole = 0");
    }
    part * 100 / whole
}

fn main() {
    println!("{}", percent(1, 4));
    println!("{}", percent(1, 0));
}
25
thread 'main' (13257569) panicked at main.rs:3:9:
percent() called with whole = 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Why the compiler said that

The first call prints 25. The second hits panic!: Rust prints the message with the file, line and column, unwinds the stack (running destructors so memory and files are cleaned up), and exits with a non-zero status. Nothing after it runs.

The fix

Panicking is right when a zero here means the caller has a bug. If a zero can legitimately arrive (from user input, say), return Result<u32, String> or Option<u32> instead and let the caller decide.

rust
fn percent(part: u32, whole: u32) -> Option<u32> {
    if whole == 0 {
        return None;
    }
    Some(part * 100 / whole)
}

fn main() {
    println!("{:?}", percent(1, 4));
    println!("{:?}", percent(1, 0));
}
02

unwrap and expect (and when they are fine)

unwrap() turns a Result<T, E> into a T, panicking on Err. expect("msg") does the same with your message in the panic, which makes the crash much easier to diagnose. Both are a deliberate statement: "if this fails, the program is broken."

rustmain.rs
fn main() {
    let port: u16 = "8080".parse().unwrap();            // a literal: cannot fail
    let retries: u32 = "3".parse().expect("retries is a hard-coded number");
    println!("port {port}, retries {retries}");

    // The safe alternatives never panic
    let bad = "eighty".parse::<u16>();
    println!("{}", bad.clone().unwrap_or(80));
    println!("{}", bad.clone().unwrap_or_default());
    println!("{}", bad.is_err());
}
Outputcompiled & run with real Rust
port 8080, retries 3
80
0
true
Error you will hit

Runtime panic: expect on an Err

rust
fn main() {
    let input = "12a";
    let n: i32 = input.parse().expect("input must be a whole number");
    println!("{n}");
}
thread 'main' (13258022) panicked at main.rs:3:32:
input must be a whole number: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Why the compiler said that

parse returned Err(ParseIntError { kind: InvalidDigit }), and expect panicked with your message followed by the error's debug form. The location points at the expect call, not inside the standard library.

The fix

Input from a user, a file or the network can always be wrong, so handle the Err (next lesson) instead of expecting success.

rust
fn main() {
    let input = "12a";
    match input.parse::<i32>() {
        Ok(n) => println!("{n}"),
        Err(e) => println!("not a number ({e})"),
    }
}
Situationunwrap / expect OK?
Tests and quick experimentsYes. A panic is a failed test, which is what you want.
A value you just checked or built yourself ("8080".parse())Yes, prefer expect with the reason it cannot fail.
Start-up config that the program cannot run withoutOften, with expect and a clear message.
User input, files, network, databasesNo. Return or handle the error.
Library code other people callAlmost never. Return a Result and let them decide.
Code review rule
In many Rust teams a bare unwrap() in production code gets flagged in review, and expect must explain why the failure is impossible. The linter Clippy can enforce this (clippy::unwrap_used).
03

Handling a Result: match and the combinators

The full form is a match with an Ok arm and an Err arm. For the common shapes there are short methods that do the same thing in one line. They mirror the Option methods from Module 04.

rustmain.rs
use std::num::ParseIntError;

fn parse_age(s: &str) -> Result<u8, ParseIntError> {
    s.trim().parse::<u8>()
}

fn main() {
    for input in ["42", " 7 ", "300", "abc", ""] {
        match parse_age(input) {
            Ok(age) if age >= 18 => println!("{input:?}: adult ({age})"),
            Ok(age) => println!("{input:?}: minor ({age})"),
            Err(e) => println!("{input:?}: {e}"),
        }
    }

    let doubled = parse_age("21").map(|a| a as u32 * 2);      // change the Ok value
    let labelled = parse_age("x").map_err(|e| format!("bad age: {e}")); // change the Err
    let fallback = parse_age("x").unwrap_or_else(|_| 0);      // compute a default
    println!("{:?} | {:?} | {}", doubled, labelled, fallback);
}
Outputcompiled & run with real Rust
"42": adult (42)
" 7 ": minor (7)
"300": number too large to fit in target type
"abc": invalid digit found in string
"": cannot parse integer from empty string
Ok(42) | Err("bad age: invalid digit found in string") | 0

The error values are different for each failure: "300" does not fit in a u8, "abc" is not a digit, and "" is empty. ParseIntError prints a readable message with {}.

MethodOn Ok(v)On Err(e)
unwrap_or(d)vd
unwrap_or_else(f)vf(e)
map(f)Ok(f(v))Err(e)
map_err(f)Ok(v)Err(f(e))
and_then(f)f(v) (which returns a Result)Err(e)
ok()Some(v)None (error discarded)
is_ok() / is_err()true / falsefalse / true
Error you will hit

Warning: an ignored Result

rust
fn save(path: &str) -> Result<(), String> {
    if path.is_empty() {
        return Err(String::from("empty path"));
    }
    Ok(())
}

fn main() {
    save("");
    println!("saved");
}
warning: unused `Result` that must be used
 --> main.rs:9:5
  |
9 |     save("");
  |     ^^^^^^^^
  |
  = note: this `Result` may be an `Err` variant, which should be handled
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
  |
9 |     let _ = save("");
  |     +++++++
Why the compiler said that

Result is marked #[must_use]. Calling save("") and dropping its return value compiles, but the failure vanishes silently and the program happily prints "saved". The compiler warns because this is almost always a bug.

The fix

Handle it (if let Err(e) = save("") { ... }), pass it up with ?, or, if you truly do not care, say so explicitly with let _ = save("");.

rust
fn save(path: &str) -> Result<(), String> {
    if path.is_empty() {
        return Err(String::from("empty path"));
    }
    Ok(())
}

fn main() {
    match save("") {
        Ok(()) => println!("saved"),
        Err(e) => println!("not saved: {e}"),
    }
}
04

The ? operator

Most of the time a function cannot fix an error itself; it should hand it to its caller. Writing that as a match each time is noisy. The ? operator does it in one character: on Ok(v) it gives you v and carries on; on Err(e) it returns early from the current function with that error (converted with From, see the custom-errors lesson).

rustmain.rs
use std::num::ParseIntError;

// Without ?
fn sum_verbose(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = match a.parse::<i32>() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    let y = match b.parse::<i32>() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

// With ?: exactly the same behaviour
fn sum(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = a.parse::<i32>()?;
    let y = b.parse::<i32>()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", sum_verbose("2", "3"));
    println!("{:?}", sum("2", "3"));
    println!("{:?}", sum("2", "three"));
    println!("{:?}", "10".parse::<i32>().and_then(|n| sum(&n.to_string(), "5")));
}
Outputcompiled & run with real Rust
Ok(5)
Ok(5)
Err(ParseIntError { kind: InvalidDigit })
Ok(15)
VisualizeWhere ? returnsStep 1 / 4
fn sum(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
// called as sum("2", "three")
Line 1

Called with a = "2", b = "three".

Variables now
a"2"
b"three"
All 4 steps as a table
StepLineWhat happenedVariables now
11Called with a = "2", b = "three".a = "2" b = "three"
22"2".parse() is Ok(2). ? unwraps it and execution continues.x = 2
33"three".parse() is Err(InvalidDigit). ? returns Err(InvalidDigit) from sum immediately.y = (never assigned)
44Never reached. The caller receives the Err.
Error you will hit

E0277: ? in a function that returns ()

rust
fn main() {
    let n: i32 = "42".parse()?;
    println!("{n}");
}
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> main.rs:2:30
  |
1 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
2 |     let n: i32 = "42".parse()?;
  |                              ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
2 |     let n: i32 = "42".parse()?;
3 |     println!("{n}");
4 +     Ok(())
  |
Why the compiler said that

? may return early with the error, so the function it is in must be able to return that error. This main returns (), which has nowhere to put it.

The fix

Change the return type to a Result (for main, Result<(), Box<dyn Error>> is the usual choice, last lesson), or handle the error right here with match or unwrap_or.

rust
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let n: i32 = "42".parse()?;
    println!("{n}");
    Ok(())
}
05

Option to Result and back

Option says "maybe missing"; Result says "maybe failed, and here is why." You often need to move between them: a HashMap::get gives you an Option, but your function reports errors with Result. ok_or(err) turns None into Err(err); .ok() goes the other way and throws the error away. ? also works on Option, inside a function that returns Option.

rustmain.rs
use std::collections::HashMap;

fn port_for(config: &HashMap<&str, &str>, service: &str) -> Result<u16, String> {
    let raw = config
        .get(service)
        .ok_or(format!("no entry for {service}"))?;      // Option -> Result
    raw.parse::<u16>()
        .map_err(|e| format!("{service}: {raw:?} is not a port ({e})"))
}

fn first_word_len(s: &str) -> Option<usize> {
    let word = s.split_whitespace().next()?;             // ? on Option
    Some(word.len())
}

fn main() {
    let config = HashMap::from([("web", "8080"), ("db", "fivefour")]);
    for svc in ["web", "db", "cache"] {
        match port_for(&config, svc) {
            Ok(p) => println!("{svc}: {p}"),
            Err(e) => println!("error: {e}"),
        }
    }
    println!("{:?} {:?}", first_word_len("hello world"), first_word_len("   "));
    let as_option: Option<u16> = "9000".parse().ok();    // Result -> Option
    println!("{:?}", as_option);
}
Outputcompiled & run with real Rust
web: 8080
error: db: "fivefour" is not a port (invalid digit found in string)
error: no entry for cache
Some(5) None
Some(9000)

ok_or builds its error even when the value is Some. If building the error is expensive (a format!, as here), ok_or_else(|| format!(...)) only builds it on None.

Error you will hit

E0277: ? on an Option inside a function that returns Result

rust
use std::collections::HashMap;

fn lookup(m: &HashMap<&str, i32>, k: &str) -> Result<i32, String> {
    let v = m.get(k)?;
    Ok(*v)
}

fn main() {
    let m = HashMap::from([("a", 1)]);
    println!("{:?}", lookup(&m, "a"));
}
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
 --> main.rs:4:21
  |
3 | fn lookup(m: &HashMap<&str, i32>, k: &str) -> Result<i32, String> {
  | ----------------------------------------------------------------- this function returns a `Result`
4 |     let v = m.get(k)?;
  |                     ^ use `.ok_or(...)?` to provide an error compatible with `Result<i32, String>`
Why the compiler said that

? on an Option would return None early, but this function returns a Result. None carries no error value, so Rust does not invent one.

The fix

Say what the error is: m.get(k).ok_or(format!("missing {k}"))?.

rust
use std::collections::HashMap;

fn lookup(m: &HashMap<&str, i32>, k: &str) -> Result<i32, String> {
    let v = m.get(k).ok_or(format!("missing {k}"))?;
    Ok(*v)
}

fn main() {
    let m = HashMap::from([("a", 1)]);
    println!("{:?}", lookup(&m, "a"));
}
06

Custom error types: Display, Error and From

A String error is fine for a script, but callers cannot match on it. Real libraries define an error enum with one variant per thing that can go wrong. To be a good citizen it implements three traits (traits are Module 07; here you only need the pattern):

  • Display — the human-readable message, used by {}.
  • std::error::Error — marks it as an error type, so it fits in Box<dyn Error> and works with other error tooling. Debug is required too; derive it.
  • From<OtherError> — tells ? how to convert a lower-level error into yours automatically.
rustmain.rs
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    Missing(String),
    BadNumber(ParseIntError),
    OutOfRange { key: String, value: i64 },
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::Missing(k) => write!(f, "missing key '{k}'"),
            ConfigError::BadNumber(e) => write!(f, "not a number: {e}"),
            ConfigError::OutOfRange { key, value } => write!(f, "{key}={value} is out of range"),
        }
    }
}

impl std::error::Error for ConfigError {}

impl From<ParseIntError> for ConfigError {       // lets ? convert automatically
    fn from(e: ParseIntError) -> Self {
        ConfigError::BadNumber(e)
    }
}

fn workers(line: Option<&str>) -> Result<i64, ConfigError> {
    let raw = line.ok_or(ConfigError::Missing(String::from("workers")))?;
    let n: i64 = raw.trim().parse()?;            // ParseIntError -> ConfigError
    if !(1..=64).contains(&n) {
        return Err(ConfigError::OutOfRange { key: String::from("workers"), value: n });
    }
    Ok(n)
}

fn main() {
    for line in [Some("8"), None, Some("lots"), Some("500")] {
        match workers(line) {
            Ok(n) => println!("workers = {n}"),
            Err(ConfigError::Missing(_)) => println!("using default 4"),
            Err(e) => println!("error: {e}"),
        }
    }
}
Outputcompiled & run with real Rust
workers = 8
using default 4
error: not a number: invalid digit found in string
error: workers=500 is out of range

The caller can react to one variant differently (a missing key falls back to a default) and print the rest. That is what a String error cannot give you.

Error you will hit

E0277: ? cannot convert the error type

rust
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    BadInput(ParseIntError),
}

fn read_count(s: &str) -> Result<u32, AppError> {
    let n = s.parse::<u32>()?;
    Ok(n)
}

fn main() {
    println!("{:?}", read_count("3"));
}
error[E0277]: `?` couldn't convert the error to `AppError`
 --> main.rs:9:29
  |
8 | fn read_count(s: &str) -> Result<u32, AppError> {
  |                           --------------------- expected `AppError` because of this
9 |     let n = s.parse::<u32>()?;
  |               --------------^ the trait `From<ParseIntError>` is not implemented for `AppError`
  |               |
  |               this can't be annotated with `?` because it has type `Result<_, ParseIntError>`
  |
note: `AppError` needs to implement `From<ParseIntError>`
 --> main.rs:4:1
  |
4 | enum AppError {
  | ^^^^^^^^^^^^^
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
Why the compiler said that

? converts the error it finds into the function's error type by calling From::from. There is no impl From<ParseIntError> for AppError, so it has no way to turn one into the other.

The fix

Add the From impl (then every ? on a parse in this crate just works), or convert at this one spot with s.parse::<u32>().map_err(AppError::BadInput)?.

rust
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    BadInput(ParseIntError),
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::BadInput(e)
    }
}

fn read_count(s: &str) -> Result<u32, AppError> {
    let n: u32 = s.parse()?;
    Ok(n)
}

fn main() {
    println!("{:?}", read_count("3"));
}
thiserror and anyhow
In real projects most teams generate this boilerplate with two crates: thiserror derives Display, Error and From for library error enums, and anyhow gives applications a single easy error type with context messages. Both expand to exactly the code above, so knowing the hand-written version is what lets you debug them.
07

Box<dyn Error>, main returning Result, and robust input

When a function can fail in several unrelated ways and callers only need to report the problem, not match on it, return Result<T, Box<dyn Error>>. Any type that implements Error converts into it with ?, so you can mix ParseIntError, io::Error and your own errors freely. main can return this type too.

rustmain.rs
use std::error::Error;

fn parse_pair(s: &str) -> Result<(i32, f64), Box<dyn Error>> {
    let (a, b) = s.split_once(',').ok_or("expected two values separated by a comma")?;
    let count: i32 = a.trim().parse()?;       // ParseIntError   -> Box<dyn Error>
    let price: f64 = b.trim().parse()?;       // ParseFloatError -> Box<dyn Error>
    Ok((count, price))
}

fn main() -> Result<(), Box<dyn Error>> {
    for s in ["3, 9.5", "3 9.5", "x, 1.0", "2, cheap"] {
        match parse_pair(s) {
            Ok((c, p)) => println!("{s:?} -> {} items, total {:.2}", c, c as f64 * p),
            Err(e) => println!("{s:?} -> error: {e}"),
        }
    }
    let (c, _) = parse_pair("7, 1.0")?;       // ? works in main now
    println!("done with {c}");
    Ok(())
}
Outputcompiled & run with real Rust
"3, 9.5" -> 3 items, total 28.50
"3 9.5" -> error: expected two values separated by a comma
"x, 1.0" -> error: invalid digit found in string
"2, cheap" -> error: invalid float literal
done with 7

Even a plain &str converts into Box<dyn Error>, which is why ok_or("...")? works on the first line.

Error you will hit

Runtime: main returns an Err

rust
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    println!("starting");
    let n: i32 = "4o2".parse()?;
    println!("got {n}");
    Ok(())
}
starting
Error: ParseIntError { kind: InvalidDigit }
Why the compiler said that

This is not a panic. When main returns Err(e), Rust prints Error: plus the Debug form of the error to stderr and exits with status 1. Anything printed before the failure (here "starting") has already gone to stdout.

The fix

That output is fine for a quick tool. For a polished CLI, handle the error at the top of main and print it with {} (Display) instead, then exit with std::process::exit(1).

rust
use std::error::Error;

fn run() -> Result<(), Box<dyn Error>> {
    let n: i32 = "4o2".parse()?;
    println!("got {n}");
    Ok(())
}

fn main() {
    if let Err(e) = run() {
        println!("error: {e}");
    }
}

Parsing input robustly

Put it together for the job every program has: reading lines a human typed. Trim whitespace, skip blanks, report bad lines with their line number and keep going, and summarise at the end. The input below is what the example reads from stdin.

rustmain.rs
use std::io::{self, BufRead};

fn parse_line(line: &str) -> Result<(String, u32), String> {
    let (name, qty) = line
        .split_once('=')
        .ok_or(format!("missing '=' in {line:?}"))?;
    let name = name.trim();
    if name.is_empty() {
        return Err(String::from("empty name"));
    }
    let qty: u32 = qty
        .trim()
        .parse()
        .map_err(|e| format!("bad quantity for {name}: {e}"))?;
    Ok((name.to_string(), qty))
}

fn main() {
    let mut total = 0;
    let mut errors = 0;
    for (i, line) in io::stdin().lock().lines().enumerate() {
        let line = line.expect("stdin is readable");
        if line.trim().is_empty() {
            continue;
        }
        match parse_line(&line) {
            Ok((name, qty)) => {
                println!("{name}: {qty}");
                total += qty;
            }
            Err(e) => {
                println!("line {}: {e}", i + 1);
                errors += 1;
            }
        }
    }
    println!("total {total}, {errors} bad line(s)");
}
Outputcompiled & run with real Rust
apples: 4
pears: 10
line 4: missing '=' in "kiwis 3"
line 5: empty name
line 6: bad quantity for plums: invalid digit found in string
figs: 1
total 15, 3 bad line(s)
Your turn

Make negative quantities a separate, friendlier error ("quantity cannot be negative") instead of the generic parse message. Hint: parse as i64 first.

panic!
Stop the thread with a message. For bugs and broken invariants, not for expected failures.
Result<T, E>
Ok(T) or Err(E): the return type of anything that can fail in an expected way.
unwrap / expect
Take the Ok value or panic. expect adds your message.
? operator
Unwrap Ok, or return the Err early from the current function after converting it with From.
ok_or / ok()
Convert Option to Result by supplying an error, and Result to Option by dropping it.
map_err
Transform the error inside a Result, leaving Ok values alone.
std::error::Error
The trait every error type implements; requires Debug and Display.
Box<dyn Error>
A boxed "any error" type. ? converts any Error into it.
#[must_use]
Attribute on Result that makes the compiler warn when you ignore one.
Quick check

Inside fn load() -> Result, you write let n: u32 = text.parse()?; and it fails to compile with E0277. What is most likely missing?

Quick check

What happens when main returns Err(e)?

Frequently asked questions

Does Rust have exceptions?
No. Expected failures are values of type Result<T, E> that the caller must handle, and bugs use panic!, which stops the thread. The ? operator gives you the convenience of exception propagation while keeping every failure visible in the function signature.
When should I use unwrap in Rust?
In tests, prototypes, and when failure is truly impossible (parsing a literal you wrote). Prefer expect("why this cannot fail") so the crash explains itself. For user input, files and network calls, return or handle the error instead.
What does the ? operator do in Rust?
On Ok(v) (or Some(v)) it evaluates to v. On Err(e) it returns early from the current function with Err(From::from(e)). The function must itself return a compatible Result or Option.

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.