Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Rust Basics and Ownership

Enums, Option and Pattern Matching

Enums model 'one of several possibilities', each variant optionally carrying data. Option replaces null, match must handle every case, and if let handles just one.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A shape calculator and a user lookup: an enum with variants that hold different data, match to handle every variant, and Option for "might not exist" without null pointers.

The idea

An enum is a type that is exactly one of several variants, and each variant can carry its own data:

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle(f64, f64, f64),
}

match compares a value against patterns and runs the first that fits. It must be exhaustive: forget a variant and the code doesn't compile. Patterns can destructure data, bind variables, use ranges (1..=9), alternatives ('a' | 'e'), guards (if x > 10), and _ for "anything else".

Rust has no null. A value that might be missing is an Option<T>: either Some(value) or None. Because it's a different type from T, the compiler forces you to handle the None case.

if let Some(x) = maybe { ... } handles one pattern when you don't care about the rest.

The code

Enums with data, exhaustive match, and Option
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle(f64, f64, f64),
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { width, height } => width * height,
        Shape::Triangle(a, b, c) => {
            let s = (a + b + c) / 2.0; // Heron's formula
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}

fn find_user(id: u32) -> Option<&'static str> {
    match id {
        1 => Some("ana"),
        2 => Some("ben"),
        _ => None,
    }
}

fn describe(n: i32) -> &'static str {
    match n {
        i32::MIN..=-1 => "negative",
        0 => "zero",
        1..=9 => "a single digit",
        x if x % 2 == 0 => "a big even number",
        _ => "a big odd number",
    }
}

fn main() {
    let shapes = [
        Shape::Circle { radius: 1.5 },
        Shape::Rectangle { width: 3.0, height: 4.0 },
        Shape::Triangle(3.0, 4.0, 5.0),
    ];
    for s in &shapes {
        println!("area: {:.2}", area(s));
    }

    for id in [1, 3] {
        match find_user(id) {
            Some(name) => println!("user {id}: {name}"),
            None => println!("user {id}: not found"),
        }
    }

    if let Some(name) = find_user(2) {
        println!("if let found {name}");
    }
    let name = find_user(9).unwrap_or("guest"); // a default for None
    println!("fallback: {name}");

    for n in [-4, 0, 7, 42, 99] {
        println!("{n} is {}", describe(n));
    }
}

Output

area: 7.07
area: 12.00
area: 6.00
user 1: ana
user 3: not found
if let found ben
fallback: guest
-4 is negative
0 is zero
7 is a single digit
42 is a big even number
99 is a big odd number

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Using _ too early in a match, hiding variants you add later; list variants explicitly when practical.
  • unwrap() on Option in real code. It panics on None; use match, if let, unwrap_or or ?.
  • Modelling states with several booleans (is_loading, is_error) instead of one enum; enums make impossible states unrepresentable.
  • Forgetting that match arms must return the same type.

Key points

  • Enums: one of several variants, each with optional data.
  • match is exhaustive and destructures data; guards and ranges add precision.
  • Option<T> (Some/None) replaces null and must be handled.
  • if let for a single pattern; unwrap_or for defaults.

Further reading: Enums and pattern matching.

Practice questions

Why doesn't Rust have null, and what replaces it?

Answer

Null references cause crashes when code forgets to check them. Rust uses Option<T> (Some(value) or None), a distinct type that the compiler forces you to handle before using the value.

What does it mean that match is exhaustive?

Answer

Every possible value must be covered by some arm; if an enum variant (or range of values) is missing, the code doesn't compile. A wildcard _ can cover the rest.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?