Rust Error Handling
Real programs fail. Files do not exist. Networks go down. Users enter bad input. Rust requires you to handle errors explicitly. The Result type is how Rust handles operations that might fail.
What Is Result?
Result<T, E> is an enum with two variants:
enum Result<T, E> {
Ok(T), ← Success: contains a value of type T
Err(E), ← Failure: contains an error of type E
}
T is the type of the success value. E is the type of the error value.
The Package Delivery Diagram
Ok(value) = [ Package delivered successfully, item inside ] Err(reason) = [ Delivery failed, reason note inside ] You always get a response. The question is which one.
A Function That Returns Result
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Answer: {}", result),
Err(msg) => println!("Error: {}", msg),
}
match divide(5.0, 0.0) {
Ok(result) => println!("Answer: {}", result),
Err(msg) => println!("Error: {}", msg),
}
}
Output:
Answer: 5 Error: Cannot divide by zero
Reading a File — A Common Result Example
use std::fs;
fn main() {
let content = fs::read_to_string("notes.txt");
match content {
Ok(text) => println!("File contents: {}", text),
Err(e) => println!("Failed to read file: {}", e),
}
}
The function fs::read_to_string returns Result<String, io::Error>. If the file exists, you get Ok(text). If it does not, you get Err(e).
The ? Operator — Propagate Errors Upward
When your function calls another function that returns Result, you often want to pass the error up to the caller instead of handling it immediately. The ? operator does this in one character.
Without ?:
fn read_file() -> Result<String, std::io::Error> {
let content = match std::fs::read_to_string("notes.txt") {
Ok(c) => c,
Err(e) => return Err(e),
};
Ok(content)
}
With ?:
fn read_file() -> Result<String, std::io::Error> {
let content = std::fs::read_to_string("notes.txt")?;
Ok(content)
}
The ? after an expression means: if this is Err, immediately return the error from this function. If it is Ok, unwrap the value and continue.
The Escalation Diagram
read_file()
↓
fs::read_to_string()?
↓
File missing → Err(e) → ? sends it back up to caller
File found → Ok(text) → content = text → continue
Useful Result Methods
unwrap()
Returns the success value or panics on error. Use only in tests or when failure truly cannot happen:
let text = fs::read_to_string("file.txt").unwrap();
expect(message)
Like unwrap(), but shows your custom message when it panics. Better for debugging:
let text = fs::read_to_string("file.txt").expect("Could not read file.txt");
unwrap_or(default)
Returns the success value or a fallback on error:
let text = fs::read_to_string("file.txt").unwrap_or(String::from("default content"));
is_ok() and is_err()
let r: Result<i32, &str> = Ok(5);
println!("{}", r.is_ok()); ← true
println!("{}", r.is_err()); ← false
Result vs Option
Type Use when ---- -------- Option<T> A value might not exist (no error reason needed) Result<T,E> An operation might fail (you want to know why) Some(v) / None ← Option Ok(v) / Err(e) ← Result
Chaining with map and and_then
Transform a Result value without a match:
let result: Result<i32, &str> = Ok(5);
let doubled = result.map(|v| v * 2);
println!("{:?}", doubled); ← Ok(10)
Use and_then when the transformation itself can fail:
let result = "42".parse::<i32>()
.and_then(|n| Ok(n * 2));
println!("{:?}", result); ← Ok(84)
Quick Reference
Result<T, E> ← Success or failure type
Ok(value) ← Successful result
Err(error) ← Failed result
result? ← Return Err early, or unwrap Ok
result.unwrap() ← Get value, panic on Err
result.expect("msg") ← Get value, panic with message on Err
result.unwrap_or(default) ← Get value, return default on Err
result.is_ok() ← true if Ok
