Skip to main content
  1. Posts/

Rust basics: Option and Result

·481 words·3 mins
Author
m58
0xm58.xyz
Table of Contents
Rust does not use null for ordinary absence, and it does not rely on exceptions for recoverable errors. Instead, it uses enums: Option<T> and Result<T, E>.

These two types show up everywhere. If you understand them early, a lot of Rust APIs stop looking strange.

Option<T> means a value may be missing
#

Option<T> has two cases:

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

Use it when “no value” is a normal, expected outcome:

fn find_port(name: &str) -> Option<u16> {
    match name {
        "http" => Some(80),
        "https" => Some(443),
        _ => None,
    }
}

This is better than sentinel values like -1 or 0, because the type makes absence explicit.

Working with Option
#

You can handle it with match:

let port = find_port("https");

match port {
    Some(value) => println!("port: {value}"),
    None => println!("unknown service"),
}

Or with if let for the one-case form:

if let Some(value) = find_port("http") {
    println!("{value}");
}

You can also transform it with methods:

let port_text = find_port("https")
    .map(|port| port.to_string())
    .unwrap_or_else(|| "n/a".to_string());

Result<T, E> means an operation can fail
#

Result also has two cases:

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

Use it when failure should carry information:

fn parse_port(input: &str) -> Result<u16, std::num::ParseIntError> {
    input.parse::<u16>()
}

Now callers must deal with both success and failure explicitly.

Handling Result
#

The direct approach is match:

match parse_port("443") {
    Ok(port) => println!("parsed: {port}"),
    Err(err) => println!("invalid port: {err}"),
}

That style is clear and explicit. It is also sometimes verbose, which is why Rust has the ? operator.

? propagates errors
#

Inside a function that returns Result, ? means:

  • if the value is Ok, unwrap it and continue
  • if the value is Err, return early with that error
fn parse_and_increment(input: &str) -> Result<u16, std::num::ParseIntError> {
    let port = input.parse::<u16>()?;
    Ok(port + 1)
}

This is one of the most common patterns in idiomatic Rust.

unwrap and expect
#

You will see these a lot:

let port = "443".parse::<u16>().unwrap();

They are acceptable in quick experiments, tests, and examples, but they panic on failure.

For real application code:

  • prefer returning Result
  • or handle the error explicitly
  • use expect("useful message") only when failure is truly unrecoverable and the message adds context

When to use which
#

Use Option<T> when the absence of a value is ordinary:

  • a lookup may not find anything
  • a configuration key may be unset
  • a list may be empty

Use Result<T, E> when the failure itself matters:

  • parsing failed
  • file I/O failed
  • a network request failed

If you need to explain why something is missing or invalid, Result is usually the better fit.

Summary
#

  • Option<T> models presence or absence.
  • Result<T, E> models success or failure.
  • match, if let, and helper methods are the main ways to work with them.
  • ? is the idiomatic way to propagate errors upward.

In Rust, these are not niche types. They are the normal way to write safe APIs.

Related

Rust basics: control flow

·665 words·4 mins
Rust control flow is more than branching and loops. The important part is that many control-flow forms are expressions, which means they produce values. That design shows up everywhere in day-to-day Rust. It makes code concise, but it also forces you to be explicit about the shapes of the values you return.