Lesson 12 of 22ยท18 min

Option and Result

Many languages have a null value that any variable might secretly hold, and forgetting to check for it is one of the most common bugs there is. Rust has no null. When a value might be missing, the type says so: it's an Option<T>. When an operation might fail, it returns a Result<T, E>, which holds either the value or an error explaining what went wrong.

Both are ordinary enums from the standard library, so everything from the enums lesson applies. What makes them powerful is that the compiler won't let you use the value inside until you've dealt with the other case. This lesson covers how to create them, how to get values out of them, and the helper methods that keep that code short.

Option<T>

Option<T> is defined in the standard library like this:

enum Option<T> {
    None,
    Some(T),
}
 
fn main() {
    let _present: Option<u8> = Option::Some(1);
    let _absent: Option<u8> = Option::None;
}

The T means Option works with any type: Option<i32>, Option<String>, Option<&str> and so on (generics get their own lesson later). An Option<T> is either Some(value) holding a T, or None holding nothing. Option, Some and None are in the prelude, so you write Some(5) and None without a prefix.

You have already met functions that return one. Many standard library methods use Option for "there might not be an answer":

fn main() {
    let word = "rustacean";
    println!("{:?}", word.find('a'));
    println!("{:?}", word.find('z'));
 
    let temperatures = [18, 21, 17];
    println!("{:?}", temperatures.last());
 
    println!("{:?}", '7'.to_digit(10));
    println!("{:?}", 'x'.to_digit(10));
 
    let tickets: u8 = 3;
    println!("{:?}", tickets.checked_sub(5));
}
Some(4)
None
Some(17)
Some(7)
None
None

find returns the byte index of the first match, last the last element, to_digit the digit a char stands for, and checked_sub the difference unless it would go below zero. Each one returns None instead of crashing or making up a value.

Returning an Option

Your own functions can do the same. Return Some(value) when there is an answer and None when there isn't:

fn divide(total: i32, parts: i32) -> Option<i32> {
    if parts == 0 {
        None
    } else {
        Some(total / parts)
    }
}
 
fn main() {
    println!("{:?}", divide(12, 4));
    println!("{:?}", divide(12, 0));
}
Some(3)
None

The signature tells every caller that divide might not produce a number, and they can't forget: an Option<i32> is not an i32, so they have to unwrap it before doing arithmetic with it.

Getting the value out of an Option

With match, if let and let else

Since Option is an enum, the pattern matching from the last lesson works on it directly:

fn divide(total: i32, parts: i32) -> Option<i32> {
    if parts == 0 {
        None
    } else {
        Some(total / parts)
    }
}
 
fn describe(parts: i32) -> String {
    let Some(share) = divide(100, parts) else {
        return String::from("can't split between nobody");
    };
    format!("everyone gets {share}")
}
 
fn main() {
    match divide(9, 3) {
        Some(share) => println!("share: {share}"),
        None => println!("no share"),
    }
 
    if let Some(share) = divide(5, 0) {
        println!("share: {share}");
    } else {
        println!("nothing to share");
    }
 
    println!("{}", describe(4));
    println!("{}", describe(0));
}
share: 3
nothing to share
everyone gets 25
can't split between nobody

Use match when both cases need real work, if let when you only care about Some, and let else when None should end the function early.

With helper methods

Writing a match every time gets long. Option has methods for the common cases:

fn main() {
    let saved_volume: Option<u8> = None;
    let volume = saved_volume.unwrap_or(50);
    println!("volume: {volume}");
 
    let nickname: Option<String> = None;
    let shown = nickname.unwrap_or_default();
    println!("nickname: {shown:?}");
 
    let port: Option<u16> = Some(8080);
    println!("is set? {}", port.is_some());
    println!("is missing? {}", port.is_none());
 
    let doubled = port.map(|p| p * 2);
    println!("doubled: {doubled:?}");
 
    let even = port.filter(|p| p % 2 == 0);
    println!("even: {even:?}");
 
    let label = port.map_or(String::from("default port"), |p| format!("port {p}"));
    println!("{label}");
}
volume: 50
nickname: ""
is set? true
is missing? false
doubled: Some(16160)
even: Some(8080)
port 8080
MethodOn Some(v)On None
unwrap_or(default)vdefault
unwrap_or_default()vthe type's default
unwrap_or_else(func)vwhat func() returns
is_some()/is_none()true/falsefalse/true
map(func)Some(func(v))None
filter(test)Some(v) if test says trueNone
and_then(func)func(v), itself an OptionNone
map_or(default, func)func(v)default

The |p| p * 2 arguments are closures, small inline functions (they get their own lesson later). map changes the value inside a Some and leaves a None alone, so you can transform an optional value without unpacking it.

and_then is for chaining steps that can each come up empty. Its function returns an Option itself, and the chain stops at the first None:

fn main() {
    let codes = ["4", "x", ""];
    for code in codes {
        let digit = code.chars().next().and_then(|c| c.to_digit(10));
        println!("{code:?} -> {digit:?}");
    }
}
"4" -> Some(4)
"x" -> None
"" -> None

For "x" the first step finds a character and the second step finds no digit. For "" the first step already returns None, so to_digit is never called.

Option<&T>

Methods that look into a collection, like get, first and last, return Option<&T>: a reference to the element, not a copy of it, since the element stays in the collection. For Copy types like numbers, .copied() turns an Option<&T> into an Option<T>; for others, .cloned() clones the value:

fn main() {
    let lap_times = vec![61, 58, 63];
    let best: Option<&i32> = lap_times.iter().min();
    let best: Option<i32> = best.copied();
    println!("{best:?}");
 
    let riders = vec![String::from("Mo"), String::from("Lu")];
    let leader: Option<String> = riders.first().cloned();
    println!("{leader:?}");
}
Some(58)
Some("Mo")

Result<T, E>

Option says "there might be no value" but not why. When something can fail for a reason the caller should know about, use Result:

enum Result<T, E> {
    Ok(T),
    Err(E),
}
 
fn main() {
    let _good: Result<u8, String> = Result::Ok(1);
    let _bad: Result<u8, String> = Result::Err(String::from("nope"));
}

Ok(value) holds the successful result, of type T, and Err(error) holds the error, of type E. Like Option, Result, Ok and Err are in the prelude.

Parsing text into a number is the classic example. parse can't know in advance whether the text is a number, so it returns a Result:

fn main() {
    let inputs = ["42", "-7", "forty", "300"];
    for input in inputs {
        match input.parse::<u8>() {
            Ok(number) => println!("{input:?} is {number}"),
            Err(error) => println!("{input:?} failed: {error}"),
        }
    }
}
"42" is 42
"-7" failed: invalid digit found in string
"forty" failed: invalid digit found in string
"300" failed: number too large to fit in target type

parse::<u8>() says which type to parse into (the ::<> is called the "turbofish"). You can also write let n: u8 = input.parse()... and let the annotation decide. The error type here is ParseIntError, and printing it with {} gives a readable message.

Returning a Result

Your own function returns Ok(value) on success and Err(error) on failure. The simplest error type is a String describing the problem:

fn check_username(name: &str) -> Result<String, String> {
    if name.len() < 3 {
        return Err(format!("{name:?} is too short"));
    }
    if !name.chars().all(|c| c.is_ascii_alphanumeric()) {
        return Err(format!("{name:?} may only use letters and digits"));
    }
    Ok(name.to_lowercase())
}
 
fn main() {
    for name in ["Ferris", "al", "crab!"] {
        match check_username(name) {
            Ok(clean) => println!("welcome, {clean}"),
            Err(reason) => println!("rejected: {reason}"),
        }
    }
}
welcome, ferris
rejected: "al" is too short
rejected: "crab!" may only use letters and digits

The caller has to look at the Result to get the username, and in doing so it sees that there is an error case to handle.

Custom error types

A String error is fine for a message, but the caller can't easily tell different failures apart. An enum of the possible errors can be matched on, so each failure can be handled differently:

#[derive(Debug)]
enum UsernameError {
    TooShort,
    BadChar(char),
}
 
fn check_username(name: &str) -> Result<String, UsernameError> {
    if name.len() < 3 {
        return Err(UsernameError::TooShort);
    }
    if let Some(bad) = name.chars().find(|c| !c.is_ascii_alphanumeric()) {
        return Err(UsernameError::BadChar(bad));
    }
    Ok(name.to_lowercase())
}
 
fn main() {
    for name in ["Ferris", "al", "crab!"] {
        match check_username(name) {
            Ok(clean) => println!("welcome, {clean}"),
            Err(UsernameError::TooShort) => println!("{name}: pick a longer name"),
            Err(UsernameError::BadChar(c)) => println!("{name}: remove the {c:?}"),
        }
    }
}
welcome, ferris
al: pick a longer name
crab!: remove the '!'

Error types in Rust usually go one step further and implement two traits: Display, which says how to print the error for a person, and std::error::Error, which marks the type as an error so it works with the rest of the error-handling ecosystem. Traits have their own lesson; for now, this is what the two impl blocks look like:

use std::fmt;
 
#[derive(Debug)]
enum UsernameError {
    TooShort,
    BadChar(char),
}
 
impl fmt::Display for UsernameError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            UsernameError::TooShort => write!(f, "username is too short"),
            UsernameError::BadChar(c) => write!(f, "{c:?} is not allowed"),
        }
    }
}
 
impl std::error::Error for UsernameError {}
 
fn main() {
    let errors = [UsernameError::TooShort, UsernameError::BadChar('!')];
    for error in errors {
        println!("error: {error}");
    }
}
error: username is too short
error: '!' is not allowed

write! works like format! but writes into the formatter f. The Error trait needs nothing of its own here, so its impl block is empty: it only requires that Debug and Display are already implemented.

Helper methods on Result

Result has most of the same helpers as Option, plus a few for the error side:

fn main() {
    let good: Result<u32, String> = "251".parse::<u32>().map_err(|e| e.to_string());
    let bad: Result<u32, String> = "2x0".parse::<u32>().map_err(|e| e.to_string());
 
    println!("{} {}", good.is_ok(), bad.is_err());
    println!("{}", bad.clone().unwrap_or(0));
    println!("{:?}", good.clone().map(|ms| ms * 1000));
    println!("{bad:?}");
 
    let halved = good.and_then(|n| {
        if n % 2 == 0 {
            Ok(n / 2)
        } else {
            Err(String::from("odd"))
        }
    });
    println!("{halved:?}");
}
true true
0
Ok(251000)
Err("invalid digit found in string")
Err("odd")
  • is_ok()/is_err() check which variant you have.
  • unwrap_or, unwrap_or_default and unwrap_or_else give you the Ok value or a fallback, throwing the error away.
  • map changes the Ok value and leaves an Err alone.
  • map_err does the opposite: it changes the error and leaves Ok alone. Here it turns a ParseIntError into a String.
  • and_then chains another step that can fail.

These methods take self, so they consume the Result. That's why the example calls .clone() before reusing good and bad.

Converting between Option and Result

Sometimes you have one and need the other.

ok() turns a Result<T, E> into an Option<T>, keeping the value and dropping the error. Use it when you don't care why something failed, only whether it did:

fn main() {
    let width: Option<u32> = "640".parse().ok();
    let height: Option<u32> = "tall".parse().ok();
    println!("{width:?} {height:?}");
}
Some(640) None

Going the other way, ok_or(error) turns an Option<T> into a Result<T, E>, using the error you give it for the None case. ok_or_else(func) does the same but only builds the error when it's needed:

use std::collections::HashMap;
 
fn main() {
    let prices = HashMap::from([("coffee", 3), ("tea", 2)]);
 
    let coffee: Result<&i32, String> = prices.get("coffee").ok_or(String::from("no coffee"));
    println!("{coffee:?}");
 
    let item = "cocoa";
    let cocoa = prices
        .get(item)
        .ok_or_else(|| format!("{item} is not on the menu"));
    println!("{cocoa:?}");
}
Ok(3)
Err("cocoa is not on the menu")

ok_or_else is the better choice when building the error does work, like the format! here, since ok_or builds it even when the value is present.

You will also see unwrap(), expect() and the ? operator in a lot of Rust code. They are the shortcuts for "give me the value, or else stop", and the next lesson covers when each one is the right choice.

Common mistakes

Using an Option as if it were the value

fn divide(total: i32, parts: i32) -> Option<i32> {
    if parts == 0 {
        None
    } else {
        Some(total / parts)
    }
}
 
fn main() {
    let share = divide(10, 2);
    let with_tip = share + 1;
    println!("{with_tip}");
}

share is an Option<i32>, and there's no + for an Option, because it might be None. Decide what None should mean first: match or if let on it, use unwrap_or for a fallback, or map to add inside the Option (share.map(|s| s + 1)).

Forgetting the None or Err case

fn main() {
    let input = "12";
    let count = match input.parse::<u32>() {
        Ok(n) => n,
    };
    println!("{count}");
}

A match on a Result must handle Err too, and a match on an Option must handle None. That's the point: you can't forget the failure case. Add the arm, or use a helper like unwrap_or if there is a sensible default.

parse without a target type

fn main() {
    match "7".parse() {
        Ok(guess) => println!("you guessed {guess}"),
        Err(_) => println!("not a number"),
    }
}

parse can produce many types, and nothing here says which one you want: guess is only printed, and lots of types can be printed. Say it with a turbofish, "7".parse::<u32>(), or with an annotation, as in let parsed: Result<u32, _> = "7".parse();.

Returning Option<&T> where Option<T> is expected

fn fastest(times: &[u32]) -> Option<u32> {
    times.iter().min()
}
 
fn main() {
    println!("{:?}", fastest(&[42, 37, 51]));
}

iter().min() returns Option<&u32>, a reference to the element inside the slice, but the function promises an Option<u32>. Add .copied() (or .cloned() for types that aren't Copy) to turn the reference into a value.

Summary

  • Rust has no null. A value that might be missing is an Option<T>: Some(value) or None.
  • An operation that can fail returns a Result<T, E>: Ok(value) or Err(error).
  • You can't use the value inside without handling the other case. Use match, if let or let else, or a helper method.
  • unwrap_or, unwrap_or_default and unwrap_or_else give a fallback; map changes the value inside; and_then chains steps that can fail; map_err changes the error.
  • Methods that look into a collection return Option<&T>; use .copied() or .cloned() to get an Option<T>.
  • Error types can be a String, or an enum that callers can match on. Implementing Display and std::error::Error makes an enum a proper error type.
  • .ok() turns a Result into an Option; .ok_or(...) and .ok_or_else(...) turn an Option into a Result.

Practise what you read

Challenges that use the ideas from this lesson.