Can you write this from memory?
Write a function signature for parse_number that takes a &str and returns Result<i32, String>.
Rust has no exceptions. Instead, it uses types: Result<T, E> for operations that can fail, Option<T> for values that might be absent. The ? operator makes error propagation clean and concise.
This approach forces you to handle errors explicitly. No silent failures, no unexpected crashes. But the syntax takes practice: When do you use ? vs match? What's the difference between unwrap and expect? How do you chain fallible operations?
This page focuses on error handling patterns until they become automatic. From basic Result matching to the ? operator and custom error types, you'll practice idiomatic Rust error handling.
The most common first error. You used ? in a function that returns ():
// WRONG: main returns () but uses ?
// fn main() {
// let config = std::fs::read_to_string("config.toml")?; // error[E0277]
// }
// RIGHT: return Result from main
fn main() -> Result<(), std::io::Error> {
let config = std::fs::read_to_string("config.toml")?;
println!("{config}");
Ok(())
}
For application entry points, Box<dyn std::error::Error> accepts any error type:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = std::fs::read_to_string("config.toml")?;
let port: u16 = config.trim().parse()?; // different error type — still works
Ok(())
}
? unwraps Ok/Some and returns early with Err/None. It replaces verbose match blocks:
// Without ?: verbose match
fn read_username() -> Result<String, std::io::Error> {
let f = std::fs::read_to_string("username.txt");
let username = match f {
Ok(s) => s,
Err(e) => return Err(e),
};
Ok(username.trim().to_string())
}
// With ?: concise
fn read_username() -> Result<String, std::io::Error> {
let username = std::fs::read_to_string("username.txt")?;
Ok(username.trim().to_string())
}
? also works with Option — returns None early if the value is absent:
fn first_word(s: &str) -> Option<&str> {
let end = s.find(' ')?; // None if no space found
Some(&s[..end])
}
fn longest_line(text: &str) -> Option<&str> {
let mut longest: Option<&str> = None;
for line in text.lines() {
let current = longest.unwrap_or("");
if line.len() > current.len() {
longest = Some(line);
}
}
longest
}
You cannot mix ? on Result and Option in the same function. Convert with .ok() (Result → Option) or .ok_or() (Option → Result). If you're coming from Python, the approach to errors is fundamentally different; see Python vs Rust error handling compared for a side-by-side breakdown.
When ? produces a different error type than your function returns, the compiler rejects it:
// WRONG: io::Error vs ParseIntError
// fn load_port() -> Result<u16, std::io::Error> {
// let s = std::fs::read_to_string("port.txt")?; // io::Error — OK
// let port = s.trim().parse::<u16>()?; // ParseIntError — error[E0277]
// Ok(port)
// }
// RIGHT Option 1: map_err to convert inline
fn load_port() -> Result<u16, String> {
let s = std::fs::read_to_string("port.txt")
.map_err(|e| format!("read failed: {e}"))?;
let port = s.trim().parse::<u16>()
.map_err(|e| format!("bad port: {e}"))?;
Ok(port)
}
// RIGHT Option 2: Box<dyn Error> accepts any error type
fn load_port_boxed() -> Result<u16, Box<dyn std::error::Error>> {
let s = std::fs::read_to_string("port.txt")?;
let port = s.trim().parse::<u16>()?;
Ok(port)
}
Chained ? calls propagate the inner error as-is, losing where the failure happened. Add context with map_err:
fn load_config() -> Result<Config, String> {
let text = std::fs::read_to_string("config.toml")
.map_err(|e| format!("failed to read config: {e}"))?;
let port: u16 = text.lines().next().unwrap_or("0")
.parse()
.map_err(|e| format!("failed to parse port: {e}"))?;
Ok(Config { port })
}
For application code, the anyhow crate provides .context() which is more ergonomic. For libraries, define concrete error types so callers can match on variants. For a printable reference of all these combinators, see the error handling cheat sheet.
Both convert Option to Result, but differ in when the error value is created:
let name: Option<&str> = Some("Alice");
// ok_or: error value computed eagerly (always created)
let result = name.ok_or("name is required");
// ok_or_else: error value computed lazily (only on None)
let result = name.ok_or_else(|| format!("name missing at line {}", line_num));
Use ok_or for cheap static errors. Use ok_or_else when the error involves allocation or computation.
Avoid match spam with these combinators:
| Combinator | Does | Example |
|---|---|---|
map | Transform Ok value | result.map(|x| x * 2) |
map_err | Transform Err value | result.map_err(|e| format!("{e}")) |
and_then | Chain fallible operations | result.and_then(|x| validate(x)) |
or_else | Try fallback on Err | result.or_else(|_| try_backup()) |
unwrap_or | Default on Err | result.unwrap_or(0) |
unwrap_or_else | Lazy default on Err | result.unwrap_or_else(|e| handle(e)) |
ok | Result → Option (drop error) | result.ok() |
inspect_err | Log error without consuming | result.inspect_err(|e| eprintln!("{e}")) |
unwrap() and expect() both panic on error. They are not always wrong:
- Tests:
.unwrap()is fine — a panic is a test failure - Prototyping:
.unwrap()keeps code short while exploring - Provably safe:
.expect("reason")when you can prove the call never fails
// Fine: "1" is always a valid u32
let one: u32 = "1".parse().expect("literal '1' is always valid");
// Fine in tests
#[test]
fn test_parse() {
let result = parse_config("valid input").unwrap();
assert_eq!(result.port, 8080);
}
In production, prefer ? for propagation. panic! is for bugs and impossible states, not for bad user input.
For libraries or complex applications, define an error enum:
use std::fmt;
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
Config(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {e}"),
AppError::Parse(e) => write!(f, "parse error: {e}"),
AppError::Config(msg) => write!(f, "config error: {msg}"),
}
}
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
// Now ? converts both error types automatically
fn load_port() -> Result<u16, AppError> {
let s = std::fs::read_to_string("port.txt")?;
let port = s.trim().parse::<u16>()?;
Ok(port)
}
The thiserror crate generates Display and From impls from attributes, reducing this boilerplate significantly. Custom error enums with From impls are a common pattern in Rust; see structs and enums for the underlying enum definition syntax.
- The Rust Book: Error Handling — panic vs recoverable errors
- The Rust Book: Propagating Errors — the ? operator
- std::result::Result — full combinator API
- std::option::Option — ok_or, ok_or_else, and more
When to Use Rust Error Handling: Result, Option, ? Operator
- Use `Result` for operations that can fail in expected ways.
- Use `Option` for values that might be absent (not errors, just optional).
- Use `?` to propagate errors up the call stack cleanly.
- Use `unwrap()` and `expect()` only when failure is truly impossible or you want to panic.
- Define custom error types for libraries or when you need rich error context.
Check Your Understanding: Rust Error Handling: Result, Option, ? Operator
How does the ? operator work and when should you use it?
`?` unwraps a Result/Option if Ok/Some, otherwise returns early with the error. Use it to propagate errors without explicit match. The function must return a compatible Result/Option.
What You'll Practice: Rust Error Handling: Result, Option, ? Operator
Common Rust Error Handling: Result, Option, ? Operator Pitfalls
- You get `thread panicked at 'called unwrap() on an Err value'` -- you used `.unwrap()` in code that can actually fail. Replace with `?` to propagate or `match` to handle the error.
- You get `error[E0277]: the ? operator can only be used in a function that returns Result or Option` -- your function returns `()`. Change the signature to return `Result<(), Error>` so `?` has somewhere to propagate.
- You silently discard errors with `let _ = fallible_op()` and bugs go unnoticed -- the `_` pattern suppresses the unused-Result warning. At minimum log the error, or propagate with `?`.
- You use `panic!` for a condition the caller could recover from -- `panic!` is for bugs and impossible states, not for bad user input. Return `Err(...)` so callers can decide what to do.
- You write `.expect("something failed")` and the panic message is useless -- the string in `expect` should explain the assumption, not describe the symptom. Write `.expect("config.toml must exist in the working directory")`.
- You chain five `?` calls and the error message gives no context about which step failed -- each `?` propagates the inner error as-is. Use `.map_err(|e| format!("failed to parse config: {e}"))` to add context at each step.
Rust Error Handling: Result, Option, ? Operator FAQ
What is the difference between unwrap and expect?
Both panic on error, but `expect("message")` includes your message in the panic output. Use `expect` when you're sure the operation should succeed. The message documents why.
When should I use ? vs match?
Use `?` when you want to propagate the error up. Use `match` when you want to handle the error locally or transform it. `?` is for "let the caller deal with it"; `match` is for "I'll handle it here".
How do I convert between error types?
Use `map_err(|e| ...)` to transform errors. Many errors implement `From<OtherError>`, so `?` can convert automatically. The `anyhow` and `thiserror` crates help with complex error handling.
What is the difference between Result and Option?
`Option` is for absence (might not exist). `Result` is for failure (something went wrong). `None` means "nothing"; `Err(e)` means "error with details".
Can I use ? in main()?
Yes! Declare main as `fn main() -> Result<(), Error>`. Return `Ok(())` for success. Errors print and return exit code 1.
Rust Error Handling: Result, Option, ? Operator Syntax Quick Reference
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}match divide(10, 2) {
Ok(result) => println!("Result: {result}"),
Err(e) => println!("Error: {e}"),
}fn read_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("file.txt")?.read_to_string(&mut s)?;
Ok(s)
}let x = some_result.unwrap(); // panics on Err
let x = some_result.expect("should never fail");let x = result.unwrap_or(0); // default on Err
let x = result.unwrap_or_else(|| compute_default());let result = op().map_err(|e| format!("operation failed: {e}"))?;let x = optional.ok_or("value was None")?;let result = op1()
.and_then(|x| op2(x))
.and_then(|y| op3(y));fn main() -> Result<(), Box<dyn Error>> {
let config = read_config()?;
run(config)?;
Ok(())
}Rust Error Handling: Result, Option, ? Operator Sample Exercises
Fill in the Result variant for the error case.
ErrFill in to create a successful Result containing the value 42.
Ok(42)Fill in the method to unwrap the Ok value.
unwrap+ 37 more exercises
Copy-ready syntax examples for quick lookup