Option, if let and let-else

Rust · Essentials · 10 min

Rust has no null; Option is how every API says 'maybe there is nothing here', so you use it constantly.

Rust has no null. A value that may be missing has type `Option<T>`, a standard enum with two variants: Some(T) and None. Because Option<i32> and i32 are different types, you cannot accidentally use a maybe-missing value as if it were there; the compiler makes you handle None.

You will meet it everywhere: v.get(i), map.get(&k), s.find('x'), iter.next() all return Option.

Ways to get at the value, from most to least explicit:

  • match opt { Some(x) => ..., None => ... } handles both cases.
  • if let Some(x) = opt { ... } handles only the case you care about, optionally with else.
  • let Some(x) = opt else { return; }; (let-else) binds x for the rest of the function and requires the else block to diverge (return, break, continue or panic). It keeps the happy path unindented.
  • Combinators transform without unpacking: opt.map(|x| x * 2), opt.unwrap_or(0), opt.unwrap_or_default(), opt.and_then(f), opt.filter(...), and opt.ok_or(err) to convert into a Result.

unwrap() returns the value or panics on None; expect("msg") does the same with a message. They are fine in tests, prototypes, or when an invariant guarantees a value, but not for input that can legitimately be missing.

In the 2024 edition, let chains combine these: if let Some(u) = user && u.active { ... }.

To look inside an Option<String> without moving it, use opt.as_deref() (gives Option<&str>) or match on &opt.

Example

fn parse_port(s: &str) -> Option<u16> {
    s.strip_prefix("port=")?.parse().ok() // ? on Option returns None early
}

fn describe(config: &[&str]) -> String {
    let Some(first) = config.first() else {
        return "empty".to_string(); // else block must diverge
    };
    if let Some(port) = parse_port(first) && port >= 1024 {
        format!("user port {port}")
    } else {
        String::from("no user port")
    }
}

fn main() {
    println!("{}", describe(&["port=8080"])); // user port 8080
    println!("{}", describe(&["port=80"]));   // no user port
    println!("{}", describe(&[]));            // empty

    let name: Option<&str> = None;
    println!("{}", name.map(|n| n.len()).unwrap_or(0)); // 0
}

Remember

  • Option<T> replaces null; the type system forces you to handle None
  • if let for one case, let ... else for an early exit, combinators for transformations
  • unwrap/expect panic on None; keep them for true invariants and tests

Watch out for

  • Calling .unwrap() on user-controlled data and crashing on the first missing value.

Check yourself

  1. What does `Some(3).map(|x| x + 1).unwrap_or(0)` return?
  2. What must the `else` block of a `let Some(x) = opt else { ... };` do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch06-03-if-let.html