Lecture 11 - Error handling in Rust

Logistics - Homework

  • HW1: Posted, corrections due next Friday
  • HW2: Feedback will trickle in, grades will be posted after all feedback is in
  • HW3: Due next Thursday

Logistics - Exam

What I've heard from you:

  • Lectures have felt rushed
  • You know you've learned a lot but you're not sure you can demonstrate it
  • We need more practice with Rust syntax and core concepts

So:

  • I'm postponing Monday's new content until WAY later in the semester
  • We'll use Monday AND Tuesday for review and practice
  • I'm dropping another homework from the schedule - remaining homeworks will all have 2 weeks to complete

Solutions to Wednesday's Activity

(See class site)

Learning Objectives

By the end of this lecture, students should be able to:

  • Understand what panic! does
  • Decide when to use panic! vs Result<T,E>
  • Use the panic! macro directly, or implicitly via unwrap or expect
  • Create functions that return Result and handle both success and error cases
  • Use match and ? operator to handle or pass along Result values

Rust's Error Philosophy: Errors as Data (TC 12:25)

Most languages: Errors are "exceptional" events that get thrown and caught

try:
    result = divide(a, b)
    # continue with result
except DivisionByZeroError:
    # handle error

Rust: Errors are just another kind of data that functions can return

#![allow(unused)]
fn main() {
match divide(a, b) {
    Ok(result) => { /* continue with result */ },
    Err(error) => { /* handle error */ },
}
}

Error Handling in Rust

Two basic options:

  • terminate when an error occurs: macro panic!(...)
  • pass information about an error: enum Result<T,E>

Option 1 : Choose to panic!

What it means to panic:

  • stops execution ("crashes")
  • starts "unwinding the stack" (more on that later)
  • prints a message to the console
  • tells us where in the code the panic occurred

Macro panic!(...)

  • Use for unrecoverable errors
fn divide(a:u32, b:u32) -> u32 {
    if b == 0 {
        panic!("I'm sorry, Dave. I'm afraid I can't do that.");
    }
    a/b
}

fn main() {
    println!("{}",divide(20,7));
    // println!("{}",divide(20,0));
}

Getting more info out of a panic!

  • Use RUST_BACKTRACE=1 cargo run to get a backtrace like this:
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
   0: rust_begin_unwind
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
   1: core::panicking::panic_fmt
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
   2: core::panicking::panic_bounds_check
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Shortcuts to panic!

Both unwrap and expect will call panic! if there is an error.

#![allow(unused)]
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
#![allow(unused)]
fn main() {
let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

Quick Quiz

1. Which #[derive()] trait lets you print an enum with {:?}?

2. Why won't this compile?

#![allow(unused)]
fn main() {
enum Status {
    Loading,
    Complete,
    Error,
}

let status = Status::Complete;
match status {
    Status::Complete => println!("Finished!"),
    Status::Error => println!("An error has occurred!"),
}
}

3. What do you think this will print?

#![allow(unused)]
fn main() {
let result = Some(42);
match result {
    Some(x) if x > 40 => println!("Large: {}", x),
    Some(x) => println!("Small: {}", x),
    None => println!("Nothing"),
}
}

Enum Result<T,E>

#![allow(unused)]
fn main() {
enum Result<T,E> {
    Ok(T),
    Err(E),
}
}

Functions can use it to

  • return a result
  • or information about an encountered error
fn divide(a:u32, b:u32) -> Result<u32, String> {
    if b != 0 {
        Ok(a / b)
    } else {
        let str = format!("Division by zero {} {}", a, b);
        Err(str)
    }
}

fn main(){
    println!("{:?}",divide(20,7));
    // println!("{:?}",divide(20,0));
}

When to use Result<T,E> and when to panic!

  • Use panic! when the error is unrecoverable
  • Use Result when you want to handle the error and continue execution

Concept: Propagating Errors

Error propagation means passing errors up through multiple layers of function calls, rather than handling them immediately at the lowest level.

Think of it like a company hierarchy:

  • Junior developer encounters a bug they can't fix, reports it to senior developer
  • Senior dev can't solve it, escalates to team lead
  • Each level decides: "Can I handle this?" or "Pass it up the chain"

Or like the court system:

  • Local court -> Appeals court -> State supreme court -> Federal supreme court
  • Each level can either handle the case or pass it to a higher authority
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Function A    │───▶│   Function B    │───▶│   Function C    │
│ (high level)    │    │ (middle level)  │    │ (low level)     │
│                 │    │                 │    │                 │
│ Can decide what │    │ Maybe can't     │    │ Detects error   │
│ to do with      │    │ handle errors   │    │ but doesn't     │
│ different errors│    │ meaningfully    │    │ know context    │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                                      │
                                                      ▼
                                              Error bubbles up!
  • Function C (low-level): Knows what went wrong, but not what to do about it
  • Function A (high-level): Has context to decide how to respond to different errors

Propagating errors in Rust

  • We are interested in the positive outcome: t in Ok(t)
  • But if an error occurs, we want to propagate it ("pass the buck")
  • This can be handled using match statements
// compute a/b + c/d
fn calculate(a:u32, b:u32, c:u32, d:u32) -> Result<u32, String> {
    let first = match divide(a,b) {
        Ok(t) => t,
        Err(e) => return Err(e),
    };
    let second = match divide(c,d) {
        Ok(t) => t,
        Err(e) => return Err(e),
    };    
    Ok(first + second)
}

fn divide(a:u32, b:u32) -> Result<u32, String> {
    if b != 0 {
        Ok(a / b)
    } else {
        let str = format!("Division by zero {} {}", a, b);
        Err(str)
    }
}

fn main(){
    println!("{:?}", calculate(16,4,18,3));
    println!("{:?}", calculate(16,0,18,3));
}

The question mark shortcut

  • Place ? after an expression that returns Result<T,E>

  • This will:

    • give the content of Ok(t)
    • or return Err(e) from the encompassing function
fn calculate(a:u32, b:u32, c:u32, d:u32) -> Result<u32, String> {
    Ok(divide(a,b)? + divide(c,d)?)
}

fn divide(a:u32, b:u32) -> Result<u32, String> {
    if b != 0 {
        Ok(a / b)
    } else {
        let str = format!("Division by zero {} {}", a, b);
        Err(str)
    }
}

fn main(){
    println!("{:?}", calculate(16,4,18,3));
    println!("{:?}", calculate(16,0,18,3));
}

Caution with ?

The ? operator can only be used in functions whose return type is compatible with the value the ? is used on.

  • If you use ? on a Result<T,E>, the function must return Result<..., E> (with the same E!)
  • If you use ? on an Option<T>, the function must return Option<...>.

Activity Time