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!vsResult<T,E> - Use the
panic!macro directly, or implicitly viaunwraporexpect - Create functions that return
Resultand handle both success and error cases - Use
matchand?operator to handle or pass alongResultvalues
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 runto 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
Resultwhen 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:
tinOk(t) - But if an error occurs, we want to propagate it ("pass the buck")
- This can be handled using
matchstatements
// 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 returnsResult<T,E> -
This will:
- give the content of
Ok(t) - or return
Err(e)from the encompassing function
- give the content of
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 aResult<T,E>, the function must returnResult<..., E>(with the sameE!) - If you use
?on anOption<T>, the function must returnOption<...>.