Real data is messy. A user might be active, suspended, or pending approval. A shape might be a circle, rectangle, or triangle. A result might be success with data, or an error.

Rust's enums let you express "this could be one of several things." Combined with pattern matching, you get one of the most powerful features in any programming language.

Basic Enums

An enum lists possible variants:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

Each variant is a constant. Create one:

let dir = Direction::Up;

Enums are perfect for modeling states:

enum Status {
    Active,
    Suspended,
    PendingApproval,
}

struct User {
    username: String,
    status: Status,
}

let user = User {
    username: String::from("alice"),
    status: Status::Active,
};

Enums with Data

Each variant can carry its own data:

enum Message {
    Quit,                      // No data
    Move { x: i32, y: i32 },  // Named fields like a struct
    Write(String),             // Single value
    ChangeColor(u8, u8, u8),  // Tuple-like
}

This is incredibly flexible. You can model almost any "one-of" situation:

let msg = Message::Move { x: 10, y: 20 };
let note = Message::Write(String::from("Hello!"));

The Option Enum — Rust's Null Alternative

This is the most important enum in Rust. It's so important it's built into the language:

enum Option<T> {
    Some(T),    // We have a value
    None,       // We don't have a value
}

Every value that might "not exist" uses Option. No null. No billion-dollar mistakes:

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("alice"))
    } else {
        None
    }
}

let user = find_user(1);  // Some("alice")
let missing = find_user(99);  // None

You must handle both cases. Rust won't let you ignore None.

Pattern Matching with match

match is like a super-powered switch statement:

let status = Status::Active;

match status {
    Status::Active => println!("User is active!"),
    Status::Suspended => println!("User is suspended"),
    Status::PendingApproval => println!("Awaiting approval"),
}

The compiler ensures you handle every possible variant. Add a new variant? You'll get a compiler error until you handle it.

Matching with Option

This is where match shines:

fn greet_user(id: u32) {
    match find_user(id) {
        Some(name) => println!("Hello, {}!", name),
        None => println!("User not found"),
    }
}

Capturing Values

Extract the data inside variants:

let msg = Message::Write(String::from("Hi!"));

match msg {
    Message::Quit => println!("Goodbye"),
    Message::Move { x, y } => println!("Move to ({}, {})", x, y),
    Message::Write(text) => println!("Text: {}", text),
    Message::ChangeColor(r, g, b) => println!("RGB: {}, {}, {}", r, g, b),
}

The _ Catch-All

Handle "anything else" concisely:

let dir = Direction::Up;

match dir {
    Direction::Up => println!("Going up!"),
    Direction::Down => println!("Going down!"),
    _ => println!("Moving sideways"),  // Covers Left and Right
}

The _ Placeholder (Ignoring Values)

Need a value but don't care what it is? Use _:

let msg = Message::Write(String::from("Hello"));

match msg {
    Message::Write(_) => println!("Got a message!"),  // Don't care about content
    _ => println!("Other message"),
}

if let — When You Only Care About One Case

Sometimes you only want to handle one variant:

let msg = Message::Write(String::from("Hi!"));

// This is cleaner than full match when you only care about one case
if let Message::Write(text) = msg {
    println!("Message: {}", text);
}

This is especially useful with Option:

let user = find_user(1);

// Instead of:
// match user {
//     Some(name) => println!("{}", name),
//     None => (),
// }

// You can write:
if let Some(name) = user {
    println!("Found: {}", name);
}

Enum Methods

Just like structs, enums can have methods:

enum Status {
    Active,
    Suspended,
    PendingApproval,
}

impl Status {
    fn is_active(&self) -> bool {
        matches!(self, Status::Active)
    }
}

let status = Status::Active;
if status.is_active() {
    println!("User is active!");
}

What's Next

Enums and pattern matching let you model real-world complexity cleanly. Next up: Error Handling — making failures explicit instead of pretending everything always works.