RustArena

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?

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

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

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

Practice it