Chapter 12

Option<T>: When a value might be missing

👋 Anyone can read and edit this exercise. Sign up to save your progress.

I looked for a joke about null pointers in Rust, but there was None.

Rust has no null. Instead, when a value might be absent, the type makes that explicit using Option<T>:

enum Option<T> {
    Some(T),
    None,
}

The compiler will not let you accidentally use a None as if it were a real value. Every time you have an Option, you have to deal with both cases. That's the whole point.

There are two main ways to unwrap an option. Pattern matching is the fundamental tool:

match find_user(id) {
    Some(name) => println!("found {name}"),
    None => println!("no such user"),
}

But for common cases there are shorter combinators:

let port = settings.port.unwrap_or(8080);     // value or fallback
let upper = name.map(|s| s.to_uppercase());   // transform if Some
let len = maybe_str.map_or(0, |s| s.len());   // transform-or-default

A note on |x| ... (closures)

Those |s| s.to_uppercase() and |s| s.len() bits are closures: anonymous functions you can pass as arguments. The pipes hold the parameters; everything after them is the body:

let add_one = |x| x + 1;
add_one(2); // 3

If the body needs multiple statements, wrap it in braces:

let greet = |name: &str| {
    let trimmed = name.trim();
    format!("hello, {trimmed}")
};

Closures show up properly in the closures chapter. For this chapter, just read |s| s.len() as "a tiny one-shot function that takes s and returns s.len()."

A useful one to know: if let lets you handle just the Some case without writing a full match:

if let Some(user) = find_user(id) {
    println!("welcome, {user}");
}

Many standard-library methods return Option. .first(), .last(), .next() on iterators, .get() on slices and maps, .find(...) on iterators. You'll meet Option everywhere.

Transforming the inside

This time you don't want a fallback value, you want to transform whatever is inside the Option. You call .len() on the inner string when it's Some. A match makes both branches explicit; iterator-style methods on Option are tidier once you spot them.

Useful from the standard library

  • Option::map applies a function inside the Some and leaves None alone. So maybe.map(|s| s.len()) produces an Option<usize>.
  • Option::map_or collapses both steps into one call: a default for None and a closure for Some. Reads as maybe.map_or(0, |s| s.len()).
  • The chapter intro explains the |s| ... closure syntax. For now read it as a tiny one-shot function from s to its body.
Exercise 1 of 3
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Returns the length if `Some`, 0 if `None`.
    fn optional_string_length(maybe_string: Option<&str>) -> usize {
        maybe_string.map_or(0, |s| s.len())
    }
    
    #[test]
    fn test_optional_string_length() {
        assert_eq!(optional_string_length(Some("hello")), 5);
        assert_eq!(optional_string_length(None), 0);
    }
    

    Producing an `Option<char>`

    Now you have to produce an Option, not consume one. Asking a string for its first character is the canonical example: if the string is empty, there is no first character, and returning some "default" char would be a lie. Option<char> is the honest type.

    You could pattern-match by hand, but the standard library has already done the work for you: text.chars() returns an iterator, and every iterator's .next() already hands you Option<Item>. Compose the two.

    Useful from the standard library

    • str::chars returns an iterator over the chars of the string.
    • Iterator::next pulls one item off the iterator and returns it as Option<Item>. For the first character, that's Option<char> and exactly the return type.
    Exercise 2 of 3
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Reveal the full solution Spoiler: the complete answer
      /// Returns the first character of `text`, or `None` if the string is empty.
      fn first_char(text: &str) -> Option<char> {
          text.chars().next()
      }
      
      #[test]
      fn test_first_char() {
          assert_eq!(first_char("hello"), Some('h'));
          assert_eq!(first_char("rust"), Some('r'));
          assert_eq!(first_char(""), None);
      }
      

      Searching a collection

      The trickiest of the three: produce an Option by searching. The iterator chapter is still ahead, but slice::iter() plus a search combinator already gets you most of the way; the matched tuple still needs to be reduced down to just the username.

      Type walk-through (this is the puzzle):

      name.as_str() turns the &String we destructured out of the tuple into the &str the signature wants.

      Useful from the standard library

      • <[T]>::iter yields shared references to the slice's items, one at a time.
      • Iterator::find takes a predicate closure and returns the first matching item as an Option.
      • Option::map transforms the inner value when present. Here it pulls the username out of the tuple and converts it to &str.
      • String::as_str is the explicit "borrow this String as &str" call.
      Exercise 3 of 3
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Reveal the full solution Spoiler: the complete answer
        /// Finds a user by ID. Returns `Some(username)` if found, `None` if not.
        fn find_user_by_id(users: &[(u32, String)], id: u32) -> Option<&str> {
            users
                .iter()
                .find(|(uid, _)| *uid == id)
                .map(|(_, name)| name.as_str())
        }
        
        #[test]
        fn test_find_user_by_id() {
            let users = [
                (1, "alice".to_string()),
                (2, "bob".to_string()),
                (3, "charlie".to_string()),
            ];
            assert_eq!(find_user_by_id(&users, 2), Some("bob"));
            assert_eq!(find_user_by_id(&users, 99), None);
        }
        

        Wrapping up `Option`

        You consumed Options with fallbacks and combinators, produced new ones from string and slice operations, and chained find and map to turn a search into the exact return type the signature asked for.

        What we learned

        • Option<T> is Rust's stand-in for "value or absence". The compiler forces both cases to be handled, which is why there's no null.
        • match is the always-works tool. For common patterns, reach for unwrap_or, map, and map_or to keep call sites short.
        • if let Some(x) = ... is the lighter alternative to match when you only care about the Some branch.
        • Option::map mirrors the iterator method of the same name: it transforms the inside if present, leaves None alone.
        • Many standard-library operations already return Option: iter().next(), slice.first(), Vec::pop, HashMap::get, iter().find(...). Once you spot them, "I have an option" is often the answer to "how do I express absence here?".
        • unwrap and expect extract the value but panic on None. Use them in tests or when you've already ruled out None; otherwise prefer the safer combinators.
        • The |x| ... syntax is a closure: a tiny anonymous function. It shows up everywhere with Option and iterators. The closures chapter covers closures in their own right.
        Next chapter 13Result<T, E>: When an operation might fail