Option, Result and Error Handling · about 45 min

Result and panic!: Recoverable and Unrecoverable Errors

Why this matters

Programs fail. Files are missing, input is malformed, disks fill up, networks drop. How a language reports failure shapes how reliable its programs are. Exceptions can be thrown from anywhere and forgotten until they crash production; C's error codes are easy to ignore. Rust splits failures into two kinds: unrecoverable bugs, which stop the program with panic!, and recoverable errors, which are ordinary return values of type Result. Because a Result is a value, you cannot use the success value without deciding what to do about the error.

panic!: stop, this is a bug

fn main() {
    let v = vec![1, 2, 3];
    let idx: usize = "7".parse().unwrap();
    println!("about to index");
    let x = v[idx];
    println!("{x}");
}
about to index

thread 'main' panicked at main.rs:5:14:
index out of bounds: the len is 3 but the index is 7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

A panic prints a message with the source location, unwinds the stack (dropping every live value, so files are closed and memory freed), and ends the program with exit status 101. You can panic yourself with panic!("message {}", value). Out-of-bounds indexing, integer overflow in debug builds, unwrap on None, and division by zero all panic.

Panic is for situations that mean the program is wrong: a broken invariant, an impossible state. Continuing would risk wrong results or corrupted data, so stopping loudly is the safe choice.

Result: this can fail, and the caller decides

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result<T, E> holds either a success value of type T or an error value of type E. You have used it since module 2: parse returns Result<T, ParseIntError>, read_line returns Result<usize, io::Error>.

fn parse_age(s: &str) -> Result<u8, String> {
    let n: i64 = match s.trim().parse() {
        Ok(n) => n,
        Err(e) => return Err(format!("{s:?} is not a number ({e})")),
    };
    if n < 0 || n > 150 {
        return Err(format!("{n} is not a plausible age"));
    }
    Ok(n as u8)
}

fn main() {
    for input in ["42", "-3", "abc", "200"] {
        match parse_age(input) {
            Ok(age) => println!("age {age}"),
            Err(msg) => println!("error: {msg}"),
        }
    }
}
age 42
error: -3 is not a plausible age
error: "abc" is not a number (invalid digit found in string)
error: 200 is not a plausible age

If you call a function returning Result and ignore the value entirely, the compiler warns:

fn might_fail() -> Result<(), String> {
    Err(String::from("oops"))
}

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

Result is marked #[must_use]. If you really mean to ignore it, write let _ = might_fail();.

Handling a Result

The same toolbox as Option, plus error-specific methods:

fn main() {
    let good: Result<i32, String> = "12".parse::<i32>().map_err(|e| e.to_string());
    let bad: Result<i32, String> = "x".parse::<i32>().map_err(|e| e.to_string());

    println!("{:?} {:?}", good, bad);
    println!("{}", bad.clone().unwrap_or(0));
    println!("{}", bad.clone().unwrap_or_else(|e| { println!("  (fallback because: {e})"); -1 }));
    println!("{:?}", good.clone().map(|n| n * 10));
    println!("{} {}", good.is_ok(), bad.is_err());
    println!("{:?}", bad.clone().ok());
    if let Err(e) = &bad {
        println!("error was: {e}");
    }
}
Ok(12) Err("invalid digit found in string")
0
  (fallback because: invalid digit found in string)
-1
Ok(120)
true true
None
error was: invalid digit found in string

(The .clone() calls are only there so the example can keep using good and bad: methods like unwrap_or, map and ok take the Result by value and consume it.)

Exiting with an error status

Command-line programs report failure through their exit status: 0 means success, anything else failure. Shell scripts and other programs check it (if ./prog; then ...). In Rust, std::process::exit(code) ends the program immediately with that status:

use std::process;

fn main() {
    let input = "not-a-number";
    let n: i32 = match input.parse() {
        Ok(n) => n,
        Err(e) => {
            eprintln!("error: cannot parse {input:?}: {e}");
            process::exit(1);
        }
    };
    println!("{n}");
}
error: cannot parse "not-a-number": invalid digit found in string

The error message goes to stderr (so it is not mixed into the program's real output), and the status is 1. Note that process::exit does not run destructors of live values; for simple programs that is fine. Lesson 3 shows a cleaner way, returning a Result from main.

Choosing between panic and Result

situationuse
bad user input, missing file, network error, invalid dataResult, the caller decides
a bug: violated invariant, "impossible" statepanic! (or unreachable!, assert!)
quick prototype, test code, examplesunwrap/expect is acceptable
you know more than the compiler (e.g. parsing a literal)expect("reason")

A useful question: could a correct program encounter this? A correct program will meet malformed input; it should handle it. A correct program will never index past the end of its own list; if it does, panic.

assert!(condition, "message") panics when a condition is false. It documents and enforces invariants:

fn average(values: &[f64]) -> f64 {
    assert!(!values.is_empty(), "average of an empty slice");
    values.iter().sum::<f64>() / values.len() as f64
}

fn main() {
    println!("{}", average(&[1.0, 2.0, 6.0]));
    println!("{}", average(&[]));
}
3

thread 'main' panicked at main.rs:2:5:
average of an empty slice
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

(Whether an empty slice should be an error the caller handles, Option<f64>, or a bug, assert!, is a design decision. For a library function, Option is friendlier.)

Common mistakes

Recap

Going further: set RUST_BACKTRACE=1 in the sandbox shell before running a panicking program and read the backtrace: it lists the chain of function calls that led to the panic.

Exercises

Exercise 1 · write code

Division that reports errors

Read two lines from stdin, each meant to be an integer. Print a / b (integer division). On problems, print a message to stderr and exit with status 1:

Write fn parse(s: &str) -> Result<i64, String> and fn divide(a: i64, b: i64) -> Result<i64, String>, and handle both results in main with match. Successful output, for 17 and 5:

3

File: main.rs

Starting point

use std::io;

fn read_line() -> String {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    s
}

fn main() {
    let a: i64 = read_line().trim().parse().unwrap();
    let b: i64 = read_line().trim().parse().unwrap();
    println!("{}", a / b);
}

Example

input

17
5

expected output

3

Example

input

seven
5

expected output

(nothing)
Hint 1

parse can match s.parse::<i64>() { Ok(n) => Ok(n), Err(_) => Err(format!(...)) }.

Hint 2

std::process::exit(1) ends the program with status 1. Print the message with eprintln! first.

Reference solution
use std::io;
use std::process;

fn read_line() -> String {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    s
}

fn parse(s: &str) -> Result<i64, String> {
    let s = s.trim();
    match s.parse::<i64>() {
        Ok(n) => Ok(n),
        Err(_) => Err(format!("not a number: {s}")),
    }
}

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

fn main() {
    let a = match parse(&read_line()) {
        Ok(n) => n,
        Err(e) => {
            eprintln!("error: {e}");
            process::exit(1);
        }
    };
    let b = match parse(&read_line()) {
        Ok(n) => n,
        Err(e) => {
            eprintln!("error: {e}");
            process::exit(1);
        }
    };
    match divide(a, b) {
        Ok(q) => println!("{q}"),
        Err(e) => {
            eprintln!("error: {e}");
            process::exit(1);
        }
    }
}

Exercise 2 · quiz

Panic or Result?

For which situations is returning a Result (rather than panicking) the right choice? Select all that apply.

Answer
  • The user typed a date in the wrong format

  • A configuration file does not exist

  • A network request timed out

Bad input, missing files and network failures are expected events in a working program: the caller should decide what to do (ask again, use a default, report and exit). A violated internal invariant means the program itself is wrong; continuing could corrupt data, so panicking is appropriate there.

Exercise 3 · write code

Skip and count bad lines

Read lines from stdin. Sum every line that parses as an i64 (after trimming). For lines that do not parse, print line N: invalid "TEXT" to stdout (N is the 1-based line number, TEXT the trimmed line), and continue. At the end print the sum and how many lines were bad:

line 2: invalid "x"
line 4: invalid "1.5"
sum: 13, bad lines: 2

for the input 10, x, 3, 1.5. Exit with status 0 normally, but with status 2 if every line was bad (and there was at least one line).

File: main.rs

Starting point

use std::io;

fn main() {
    for line in io::stdin().lines() {
        let line = line.unwrap();
    }
}

Example

input

10
x
3
1.5

expected output

line 2: invalid "x"
line 4: invalid "1.5"
sum: 13, bad lines: 2

Example

input

a
b

expected output

line 1: invalid "a"
line 2: invalid "b"
sum: 0, bad lines: 2
Hint 1

for (i, line) in io::stdin().lines().enumerate() gives 0-based numbers; print i + 1.

Hint 2

{:?} prints the text with quotes.

Hint 3

Count total lines too; after printing the summary, std::process::exit(2) when all were bad.

Reference solution
use std::io;

fn main() {
    let mut sum: i64 = 0;
    let mut bad = 0;
    let mut total = 0;
    for (i, line) in io::stdin().lines().enumerate() {
        let line = line.unwrap();
        let text = line.trim();
        total += 1;
        match text.parse::<i64>() {
            Ok(n) => sum += n,
            Err(_) => {
                println!("line {}: invalid {:?}", i + 1, text);
                bad += 1;
            }
        }
    }
    println!("sum: {sum}, bad lines: {bad}");
    if total > 0 && bad == total {
        std::process::exit(2);
    }
}