Result and the ? operator in Rust
How Rust handles errors with Result<T, E>: Ok and Err, match, the ? operator for early returns, main returning Result, and map_err.
Rust has no exceptions. A function that can fail returns Result<T, E>, an enum with two variants:
enum Result<T, E> {
Ok(T), // success, holding the value
Err(E), // failure, holding the error
}
The failure is part of the function's type, so callers can see it in the signature and the compiler makes sure they deal with it. Ignoring a Result entirely gives the warning unused Result that must be used.
Handling a Result with match
fn main() {
for input in ["42", "forty-two"] {
match input.parse::<i32>() {
Ok(n) => println!("{input:?} -> {n}"),
Err(e) => println!("{input:?} -> error: {e}"),
}
}
}
"42" -> 42
"forty-two" -> error: invalid digit found in string
The ? operator
Most of the time a function can't fix an error itself and should pass it to its caller. Writing a match for that each time is noisy, so Rust has ?:
let port: u16 = text.parse()?;
means: if parse returned Ok(v), give me v; if it returned Err(e), return Err(e) from this function right now. Before returning, ? converts the error with From::from, so a function returning Result<T, MyError> can use ? on any error that converts into MyError.
? only works inside a function that returns Result (or Option):
use std::fs::File;
fn main() {
let f = File::open("config.toml")?;
println!("{f:?}");
}
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
main can return a Result
Change main's signature and ? works there too:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = std::fs::read_to_string("config.toml")?;
println!("{} bytes", config.len());
Ok(())
}
If main returns Err, Rust prints the error (in its Debug form) and exits with a non-zero code. Box<dyn Error> accepts any error type, which is convenient in small programs; applications at work often use anyhow::Result for the same purpose.
Combinators
| Method | Use |
|---|---|
map(|v| ...) |
transform the success value |
map_err(|e| ...) |
transform the error, e.g. into your own type or a message |
and_then(|v| ...) |
chain another fallible step |
unwrap_or(default), unwrap_or_else(|e| ...) |
fall back to a value |
ok() |
turn it into an Option, dropping the error |
is_ok(), is_err() |
test without taking it apart |
expect("why") |
panic with a message on Err |
When to panic instead
panic!, unwrap() and expect() stop the program (or thread). Use them for bugs (a broken invariant, an index you just checked) and in tests and prototypes. Use Result for anything that can go wrong in normal operation: missing files, bad input, network failures.
Common mistakes
unwrap()everywhere in real code. One bad input line crashes the whole program. Propagate with?and decide at the top what to do.- Using
?on anOptionin a function returningResult: E0277, "the?operator can only be used onResults, notOptions". Convert with.ok_or(...)or.ok_or_else(...). - Error types that don't convert: "
?couldn't convert the error toMyError". ImplementFrom<TheirError> for MyError, or usemap_err.
Example
use std::num::ParseIntError;
#[derive(Debug)]
struct Order {
item: String,
qty: u32,
}
fn parse_qty(text: &str) -> Result<u32, ParseIntError> {
let qty = text.trim().parse::<u32>()?;
Ok(qty)
}
fn parse_order(line: &str) -> Result<Order, String> {
let (item, qty) = line.split_once(',').ok_or(format!("missing comma in {line:?}"))?;
let qty = parse_qty(qty).map_err(|e| format!("bad quantity in {line:?}: {e}"))?;
if qty == 0 {
return Err(format!("zero quantity in {line:?}"));
}
Ok(Order { item: item.trim().to_string(), qty })
}
fn total_qty(lines: &[&str]) -> Result<u32, String> {
let mut total = 0;
for line in lines {
total += parse_order(line)?.qty;
}
Ok(total)
}
fn main() {
for line in ["potion, 3", "sword 1", "shield, two", "arrow, 0"] {
match parse_order(line) {
Ok(order) => println!("ok: {} x{}", order.item, order.qty),
Err(e) => println!("error: {e}"),
}
}
println!("{:?}", total_qty(&["a, 1", "b, 2"]));
println!("{:?}", total_qty(&["a, 1", "b, x"]));
println!("default: {}", parse_qty("oops").unwrap_or(1));
}
Output:
ok: potion x3
error: missing comma in "sword 1"
error: bad quantity in "shield, two": invalid digit found in string
error: zero quantity in "arrow, 0"
Ok(3)
Err("bad quantity in \"b, x\": invalid digit found in string")
default: 1