Skip to content
← All Rust articles

Rust explainer

Option and Result: absence and failure as values

About 5 min to read. Allow extra time to work through examples.

Use Option for a value that may be absent and Result for an operation that may fail. Learn how matching, methods, and the question-mark operator handle both.

This article uses Rust. You can make it your practice mode to browse related content across the site.

A lookup may find nothing. Parsing a number may fail. Rust commonly represents these outcomes in the return value:

Type Variants What it communicates
Option<T> Some(T), None A value may be absent
Result<T, E> Ok(T), Err(E) An operation may succeed or return an error

Both are standard-library enums. Their variants carry values, so you can store them in fields, return them from functions, and process them with ordinary pattern matching.

Read the variants as alternatives

Their essential structure is:

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

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

T and E are type parameters. For example, Option<String> may hold a string, while Result<u16, ParseIntError> holds either a number or a parsing error. You do not redefine these types in your program; they and their variants are available through Rust's prelude.

You can define similar enums yourself. That is useful for understanding their structure, but a custom enum does not automatically inherit their methods, documented layout guarantees, or support for ?. Implementing the Try trait for your own type is currently unstable.

Handle absence explicitly

A String and an Option<String> are different types. A function returning the latter tells its caller that absence is a normal outcome.

fn first_name(names: &[String]) -> Option<&str> {
    names.first().map(String::as_str)
}

fn main() {
    let names = vec![String::from("Ada")];
    assert_eq!(first_name(&names), Some("Ada"));
    assert_eq!(first_name(&[]), None);

    match first_name(&names) {
        Some(name) => println!("Hello, {name}"),
        None => println!("No names supplied"),
    }
}

The match handles both variants. For a custom enum, adding a variant makes an exhaustive match without a catch-all incomplete. A wildcard arm can already cover new variants, so not every match must change.

Rust references cannot be null, but raw pointers can. The useful distinction here is that ordinary references and optional references have different types. Returning T instead of Option<T> rules out an absent return value; it does not promise that the function cannot panic or fail to return.

Keep the reason for a failure

None carries no explanation. Use Result when the caller needs an error value, as in this parser:

use std::num::ParseIntError;

fn parse_ports(inputs: &[&str]) -> Result<Vec<u16>, ParseIntError> {
    inputs.iter().map(|text| text.parse::<u16>()).collect()
}

fn main() {
    assert_eq!(parse_ports(&["80", "443"]), Ok(vec![80, 443]));
    assert!(parse_ports(&["80", "invalid"]).is_err());
}

Collecting into Result<Vec<_>, _> stops at the first error. Collecting into Vec<Result<_, _>> would instead keep each input's outcome.

An Err does not unwind the stack by itself. The caller decides whether to recover, report it, store it, or return it. Methods such as ok() and iteration over a Result can discard errors, so choose them deliberately when the error information is not needed.

Result has a must_use annotation: leaving a result expression unused normally warns, and this site's warnings-as-errors gate rejects that. Explicitly discarding it with let _ = ...; is permitted. The annotation encourages checking results; it cannot prove that a program handles every failure well.

Propagate with the question-mark operator

In a function returning Result, ? extracts an Ok value or returns an error early. A compatible error conversion must exist when the enclosing function uses a different error type.

use std::num::ParseIntError;

fn parse_pair(left: &str, right: &str) -> Result<(u16, u16), ParseIntError> {
    let left = left.parse::<u16>()?;
    let right = right.parse::<u16>()?;
    Ok((left, right))
}

fn main() {
    assert_eq!(parse_pair("80", "443"), Ok((80, 443)));
    assert!(parse_pair("invalid", "443").is_err());
    assert!(parse_pair("80", "invalid").is_err());
}

The function advertises failure in its return type while leaving the reporting policy to its caller. ? also works with Option in an Option-returning function, where None causes an early return. It does not automatically turn None into an error; ok_or or ok_or_else can supply that error explicitly.

Decide what unwrap means in your code

unwrap returns the contained success value and panics on None or Err. expect does the same with a custom message. Depending on the panic strategy and surrounding code, a panic may unwind or abort; it is not synonymous with an ordinary error return.

Use these methods when failure would violate an established invariant, including in tests where failure should stop the test. For expected missing input or environmental failures, choose an appropriate fallback or return an error. An expect message should explain why success was expected.

For example, if a command-line tool cannot locate its configuration directory, reasonable policies include an explicitly documented fallback, asking the user to specify a path, or reporting an error and exiting. Silently using the current directory is appropriate only if that is the tool's intended behavior.

What these types cost

Neither type requires a heap allocation merely to represent its variants. Its payload may allocate, and handling outcomes can require branches. There is no general guarantee of zero runtime cost.

Some types have unused representations called niches. Rust documents a same-size guarantee for examples such as Option<&u8> and &u8, using the null representation for None. Do not generalize that guarantee to every enum or assume a fixed size for Option<i32> on every target.

The next lessons practice matching and transforming these values, then designing error types. Those error types may be structs or enums; the Error and Display traits help integrate them with other libraries.

References: Option representation, Result, Try, and panic behavior.

step 1 of 24

Errors Are Values

Up next Matching on Option