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.