Option vs Result in Rust: which one should you return?
When to return Option<T> and when to return Result<T, E> in Rust, how to convert between them with ok_or and ok(), and using ? with each.
Both types say "you might not get a value", but they mean different things:
Option<T> |
Result<T, E> |
|
|---|---|---|
| Variants | Some(T) / None |
Ok(T) / Err(E) |
| Means | the value may be absent, and that's normal | the operation may fail, and here's why |
| Carries a reason | no | yes, the E value |
| Typical std examples | v.get(i), map.get(k), s.find(c), iter.max() |
s.parse(), File::open, fs::read_to_string |
| Ignoring it | allowed | warns unused Result that must be used |
The rule of thumb
Ask: if there's no value, would the caller want to know why?
- Looking up a user by id, the first element of a list, the position of a character: absence is an ordinary answer, and there's only one way to be absent. Return
Option. - Parsing input, reading a file, calling an API, validating a form: there are several ways to fail and the caller (or the user) needs to know which. Return
Result.
A function that returns Result<T, ()> or Option for something that can fail in several ways is a design smell; one returning Result<T, String> just to say "not found" is the opposite smell.
Sometimes you need both: Result<Option<User>, DbError> means "the query might fail, and if it worked, the user might not exist". That's a perfectly good type.
Converting between them
fn main() {
let maybe_port: Option<&str> = Some("8080");
let missing: Option<&str> = None;
let a: Result<&str, String> = maybe_port.ok_or("no port".to_string());
let b: Result<&str, String> = missing.ok_or_else(|| "no port".to_string());
println!("{a:?} {b:?}");
let parsed: Option<u16> = "8080".parse().ok();
let failed: Option<u16> = "http".parse().ok();
println!("{parsed:?} {failed:?}");
}
Ok("8080") Err("no port")
Some(8080) None
option.ok_or(err)/option.ok_or_else(|| err):NonebecomesErr(err).result.ok(): keep the value, drop the error.result.err()does the opposite.transpose()swapsOption<Result<T, E>>andResult<Option<T>, E>, handy when parsing an optional field.
The ? operator works on both, but not mixed
In a function returning Option, ? on an Option returns None early. In a function returning Result, ? on a Result returns the error early. Using ? on an Option inside a function returning Result doesn't compile:
fn first_even(v: &[i32]) -> Result<i32, String> {
let x = v.iter().find(|n| *n % 2 == 0)?;
Ok(*x)
}
fn main() {
println!("{:?}", first_even(&[1, 2]));
}
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
Convert first, adding the reason the Option couldn't: .find(...).ok_or("no even number")?.
Common mistakes
- Using sentinel values like
-1or""for "not found". That's whatOptionis for. unwrap()on either one in library code. Return it to the caller and let them decide.- Losing information with
.ok(). Turning a parse error intoNoneis fine for "use a default if invalid"; it's bad when the user needs to see the error.
Example
use std::collections::HashMap;
#[derive(Debug)]
enum LoginError {
UnknownUser(String),
WrongPassword,
}
// Absence is normal here: Option.
fn find_email(users: &HashMap<&str, (&str, &str)>, name: &str) -> Option<String> {
users.get(name).map(|(email, _)| email.to_string())
}
// Failure needs a reason: Result.
fn login(users: &HashMap<&str, (&str, &str)>, name: &str, password: &str) -> Result<String, LoginError> {
let (_, stored) = users.get(name).ok_or_else(|| LoginError::UnknownUser(name.to_string()))?;
if *stored != password {
return Err(LoginError::WrongPassword);
}
Ok(format!("welcome back, {name}"))
}
fn main() {
let mut users = HashMap::new();
users.insert("ferris", ("[email protected]", "crab123"));
println!("{:?}", find_email(&users, "ferris"));
println!("{:?}", find_email(&users, "gopher"));
for (name, password) in [("ferris", "crab123"), ("ferris", "hunter2"), ("gopher", "go")] {
match login(&users, name, password) {
Ok(greeting) => println!("{greeting}"),
Err(LoginError::UnknownUser(who)) => println!("no user named {who}"),
Err(e) => println!("login failed: {e:?}"),
}
}
let port: u16 = "80a".parse().ok().unwrap_or(8080);
println!("port {port}");
}
Output:
Some("[email protected]")
None
welcome back, ferris
login failed: WrongPassword
no user named gopher
port 8080