Error Handling
Rust splits failure into two lanes: stop the thread with panic!, or hand a Result back so the caller can choose.
<!-- hal:authoritative:yaml -->
*Rust splits failure into two lanes: stop the thread with panic!, or hand a Result back so the caller can choose.*
§I — Frame
Duha session 10. TRPL Chapter 9 is one syllabus row, so this fire takes the whole chapter at chapter pace. Session 09 left you with Vec, String, and HashMap. Keep those types in hand; today the operations around them start returning failure as data.
Most languages throw exceptions and hope someone catches them. Rust names two kinds of trouble up front:
- Unrecoverable — the program is in a bad state. Use
panic!. - Recoverable — the operation failed in a way the caller might handle. Use
Result<T, E>.
By the end, propagate with ?, choose panic! versus Result for a concrete case, and keep main honest so ? can live at the top of the binary. That is the syllabus test.
§II — Unrecoverable: panic!
panic! ends the current thread. By default Rust unwinds: it walks the stack, runs destructors, and cleans up. Set panic = 'abort' under [profile.*] in Cargo.toml if you want the process to abort without unwind (smaller binary, no destructor walk on panic).
You can call it yourself:
fn main() {
panic!("crash and burn");
}
Or you can trip a library panic. Indexing past the end of a vector is the book’s example:
fn main() {
let v = vec![1, 2, 3];
v[99];
}
That is not a Result. The index is a bug: there is no sensible recovery inside the indexing operator. Set RUST_BACKTRACE=1 when you need the stack frames that led to the panic. Read from the top of your own frames until the first library frame, then walk outward.
Use panic! when continuing would be wrong. Do not use it as the everyday return path for “file missing” or “user typed letters.”
§III — Recoverable: Result<T, E>
Result is an enum in the prelude, the same family as Option:
enum Result<T, E> {
Ok(T),
Err(E),
}
File::open returns Result<File, std::io::Error>. Success is Ok(file). Failure is Err(error) with a kind you can inspect.
Match the outcome
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
That match still panics on any error. Often you want a branch: if the file is missing, create it; otherwise fail hard.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = match File::open("hello.txt") {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
other_error => {
panic!("Problem opening the file: {other_error:?}");
}
},
};
}
Nested match works and teaches the types. Closures clean it up with unwrap_or_else:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
unwrap and expect
unwrap is match-and-panic in one method: Ok unwraps; Err panics with a default message. expect does the same and lets you name why success was assumed:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
In examples and early prototypes, either is fine. In production-quality code, prefer expect with a sentence that states the invariant. Save bare unwrap for cases where the panic message does not need to teach the next reader.
§IV — Propagate with ?
When a helper cannot decide how to recover, return the error to the caller. Manual form:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
The return type is Result<String, io::Error> because both File::open and read_to_string fail with io::Error. The caller picks panic, a default, or another source.
The ? operator is that pattern in one character:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
On Ok, ? yields the inner value. On Err, it returns early from the function. It also runs From::from on the error, so a function can return one error type while callees produce others, as long as the conversion exists.
You can chain:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
Or skip the file handle and call fs::read_to_string("hello.txt")? when you only need the text.
? also works with Option in a function that returns Option, with the same early-return shape for None. You cannot freely mix Result and Option behind one ? without an explicit bridge (.ok(), .ok_or(...), and friends).
§V — Keep main honest
? only works in functions whose return type can accept the early Err (or None). main may return a Result:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
Box<dyn Error> is a trait object that can hold many error types. When main returns Err, the binary exits with a nonzero code and prints the error. That is the honest top of a binary: failure is a value, not a silent unwrap.
§VI — When to panic, when to return Result
The last section of the chapter is judgment, not syntax.
**Prefer Result** when failure is an expected possibility the caller must decide: missing files, bad input, network refusal, parse failure on user text.
**Prefer panic!** (or a type that makes the bad state unrepresentable) when the program is in a broken invariant: out-of-bounds index inside trusted code, a contract your own API promised and the caller violated after you documented it, or a situation where continuing would corrupt data or put a user at risk.
Examples and tests may use unwrap / expect freely. Hard-coded values you know parse (for example "127.0.0.1".parse::<std::net::IpAddr>()) can use expect because you have more information than the compiler.
When a range or shape must hold, put the check in a type. The book’s Guess newtype panics in new if the value is outside 1..=100, so later code never re-checks the range:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if !(1..=100).contains(&value) {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
Invalid values never exist as Guess. That is panic used as a construction gate, not as everyday control flow. Prefer returning Result from new when callers must recover from bad input without aborting the thread.
§VII — One complete proof
- Call
panic!("demo")and read aRUST_BACKTRACE=1run once so the frames are familiar. - Open a file with
matchonResult, then rewrite the same path with?inside a helper that returnsResult<..., io::Error>. - Replace one
unwrapwithexpect("...")and state the invariant in the message. - Change
mainto-> Result<(), Box<dyn Error>>and use?at the top level.
When those four hold, Chapter 9’s selected depth is done.
§VIII — Closing
Failure is either a stop or a value. panic! stops the thread when the state is not worth continuing. Result carries success or error so the caller can choose. ? propagates without nested match, and an honest main that returns Result lets that propagation reach the process exit. Collections from Chapter 8 now sit behind operations that fail in the open.
Done-criteria: propagate with ?, choose panic! versus Result, and keep main honest.