Option & Result

Reviewed & published by Brayan K

Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and it has no null and no exceptions. Instead, absence and failure are modelled with two enums: Option and Result.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll use Option<T> for maybe-missing values and Result<T, E> for operations that can fail, and handle both safely.

What You'll Learn in This Lesson

1️⃣ Option: Values That Might Be Missing

Option<T> is an enum with two variants: Some(value) when a value exists and None when it doesn't. Because it's a distinct type, the compiler forces you to deal with the None case before you can touch the value — eliminating null-pointer bugs.

fn main() {
    // Option<T> models a value that MIGHT be absent. It has two variants:
    //   Some(value)  — there is a value
    //   None         — there is nothing
    // Rust has no null; Option is how absence is expressed safely.

    let numbers = vec![10, 20, 30];

    // .get(index) returns Option<&i32>: Some(&v) if in range, None if not.
    let found: Option<&i32> = numbers.get(1);    // Some(&20)
    let missing: Option<&i32> = numbers.get(99); // None

    // match forces you to handle BOTH cases.
    match found {
        Some(v) => println!("found: {}", v),     // -> found: 20
        None => println!("nothing there"),
    }

    // Handy helpers:
    println!("missing or default: {}", missing.unwrap_or(&-1));  // -> -1
    println!("is some? {}", found.is_some());     // -> is some? true
}

Methods like .get() return an Option precisely because the index might be out of range. Helpers such as unwrap_or let you supply a fallback without a full match.

2️⃣ Result: Operations That Can Fail

Result<T, E> is the failure-aware sibling of Option. It is Ok(value) on success or Err(error) on failure, where the error carries information about what went wrong. Functions that can fail — parsing, dividing, file I/O — return a Result.

// Result<T, E> models an operation that can FAIL:
//   Ok(value)  — success, carrying the value
//   Err(error) — failure, carrying an error
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("cannot divide by zero"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    // Handle a Result with match.
    match divide(10.0, 2.0) {
        Ok(value) => println!("10 / 2 = {}", value),   // -> 10 / 2 = 5
        Err(e) => println!("error: {}", e),
    }

    match divide(1.0, 0.0) {
        Ok(value) => println!("result = {}", value),
        Err(e) => println!("error: {}", e),            // -> error: cannot divide by zero
    }

    // .parse() returns a Result. unwrap_or supplies a fallback on Err.
    let n: i32 = "42".parse().unwrap_or(0);
    let bad: i32 = "oops".parse().unwrap_or(0);
    println!("parsed: {} and {}", n, bad);             // -> parsed: 42 and 0
}

The difference from Option: Err tells you why it failed. That's why divide returns a descriptive message instead of just None.

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — finish the Option and Result handling, then run it.
fn find_even(numbers: &[i32]) -> Option<i32> {
    for &n in numbers {
        if n % 2 == 0 {
            return ___(n);     // 👉 wrap n in the "has a value" variant
        }
    }
    None                       // no even number found
}

fn safe_sqrt(x: f64) -> Result<f64, String> {
    if x < 0.0 {
        ___(String::from("negative"))   // 👉 the failure variant
    } else {
        Ok(x.sqrt())
    }
}

fn main() {
    match find_even(&[1, 3, 4, 7]) {
        Some(v) => println!("first even: {}", v),   // expected: first even: 4
        None => println!("none found"),
    }

    println!("sqrt: {:?}", safe_sqrt(9.0));   // expected: sqrt: Ok(3.0)
}

// ✅ Expected output:
//    first even: 4
//    sqrt: Ok(3.0)

Mini-Challenge: A Safe Lookup

Return an Option<char> and handle both cases. Run it with cargo run.

// 🎯 MINI-CHALLENGE: a safe lookup.
//
// 1. Write fn first_char(s: &str) -> Option<char> that returns Some(first
//    character) or None for an empty string. Hint: s.chars().next() already
//    returns an Option<char>.
// 2. In main, call it on "rust" and on "" (empty).
// 3. For each, use match (or unwrap_or) to print the result:
//      - print "First char: r" when there is one
//      - print "No characters" when empty
//
// ✅ Example output:
//    First char: r
//    No characters

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Option & Result

GoalRust Syntax
Has a valueSome(5)
No valueNone
SuccessOk(5)
FailureErr("oops")
Fallbackopt.unwrap_or(0)
Check presenceopt.is_some()

🎉 Lesson Complete!

Practice quiz

What are the two variants of Option<T>?

  • Ok and Err
  • Just and Nothing
  • Some and None
  • True and False

Answer: Some and None. Option is either Some(value) or None.

Why does Rust use Option instead of null?

  • The compiler forces you to handle the None case
  • It is faster
  • It uses less memory
  • null is deprecated

Answer: The compiler forces you to handle the None case. Option is a distinct type, so missing values must be handled at compile time.

What does vec![10,20,30].get(99) return?

  • 20
  • Some(99)
  • a panic
  • None

Answer: None. .get on an out-of-range index returns None.

What does numbers.get(99).unwrap_or(&-1) return?

  • 99
  • -1
  • None
  • a panic

Answer: -1. unwrap_or supplies the fallback -1 when the value is None.

What are the two variants of Result<T, E>?

  • Ok and Err
  • Some and None
  • Pass and Fail
  • Yes and No

Answer: Ok and Err. Result is Ok(value) on success or Err(error) on failure.

What is the key difference between Option and Result?

  • Option is faster
  • Result has no None
  • Result carries an error describing the failure
  • There is no difference

Answer: Result carries an error describing the failure. Result's Err carries error info; Option's None carries none.

What does "oops".parse::<i32>().unwrap_or(0) return?

  • oops
  • 0
  • None
  • a panic

Answer: 0. parse fails, so unwrap_or returns the fallback 0.

Why is calling unwrap() risky?

  • It is slow
  • It returns null
  • It never compiles
  • It panics on None or Err

Answer: It panics on None or Err. unwrap panics and crashes the program on a None or Err.

What does Some(5).map(|x| x * 2) produce?

  • 10
  • Some(10)
  • Some(5)
  • None

Answer: Some(10). map transforms the inner value while keeping it wrapped as Some.

When should you prefer Result over Option?

  • When absence is normal
  • Always
  • When you need to know why something failed
  • Never

Answer: When you need to know why something failed. Use Result when you need the reason for a failure; Option for plain absence.

Continue this course

Frequently asked questions

Why does Rust use Option instead of null?

Null references are a famous source of crashes because the type system does not force you to check for them — you can accidentally use a null and the program blows up at runtime. Rust has no null. Instead, a value that might be absent has type Option<T>, which is either Some(value) or None. Because Option is a distinct type, the compiler makes you handle the None case before you can get at the value, so 'forgot to check for missing' becomes a compile error rather than a runtime crash.

What is the difference between Option and Result?

Option<T> represents the presence or absence of a value: Some(value) or None. It answers 'is there something here?'. Result<T, E> represents success or failure of an operation that can go wrong: Ok(value) or Err(error). The key difference is that Result's failure case carries an error describing what went wrong, while Option's None carries no information. Use Option when absence is normal and expected; use Result when you need to know why something failed.

What does unwrap do, and why is it risky?

unwrap extracts the inner value from a Some or an Ok. But if you call it on a None or an Err, it panics and crashes the program. That makes it convenient for quick prototypes, examples, and tests where you are certain a value is present, but dangerous in real code where the absence or error is possible. Safer alternatives include unwrap_or(default), match, if let, and the ? operator, which you will learn in the error-handling lesson.

What does unwrap_or do?

unwrap_or(default) returns the inner value when you have Some or Ok, and returns the default you provide when you have None or Err — without ever panicking. It is a clean way to supply a fallback, such as 'value.parse().unwrap_or(0)' to default a failed parse to zero. There are related methods too: unwrap_or_else takes a closure that computes the default lazily, and unwrap_or_default uses the type's standard default value.