In the previous tutorial, we learned structs — types where every value has the same fields. But sometimes a value can be one of several different kinds. That is what enums are for.

Enums in Rust are much more powerful than in most languages. In Java or Kotlin, enums are just named constants. In Rust, each variant can hold different data. Combined with pattern matching, enums become one of Rust’s best features.

Defining an Enum

An enum defines a type that can be one of several variants:

enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let heading = Direction::North;
    print_direction(heading);
}

fn print_direction(dir: Direction) {
    match dir {
        Direction::North => println!("Going north"),
        Direction::South => println!("Going south"),
        Direction::East => println!("Going east"),
        Direction::West => println!("Going west"),
    }
}

Each variant is accessed with EnumName::Variant. Unlike C enums, Rust enum variants are not integers by default — they are proper types.

Enums with Data

Here is where Rust enums get powerful. Each variant can hold different data:

enum Message {
    Quit,                        // No data
    Echo(String),                // One String
    Move { x: i32, y: i32 },    // Named fields (like a struct)
    Color(u8, u8, u8),           // Three values (like a tuple)
}

One enum, four variants, each with different data. In other languages you would need a class hierarchy or tagged unions for this.

fn main() {
    let messages = vec![
        Message::Quit,
        Message::Echo(String::from("hello")),
        Message::Move { x: 10, y: 20 },
        Message::Color(255, 0, 128),
    ];

    for msg in &messages {
        process_message(msg);
    }
}

The Match Expression

match is how you work with enums. It checks which variant a value is and runs the right code:

fn process_message(msg: &Message) {
    match msg {
        Message::Quit => {
            println!("Quit message received");
        }
        Message::Echo(text) => {
            println!("Echo: {}", text);
        }
        Message::Move { x, y } => {
            println!("Move to ({}, {})", x, y);
        }
        Message::Color(r, g, b) => {
            println!("Color: rgb({}, {}, {})", r, g, b);
        }
    }
}

Each arm of the match destructures the variant. The variables text, x, y, r, g, b are bound to the data inside the variant.

Match Is Exhaustive

You must handle every variant. If you forget one, the compiler gives an error:

fn direction_name(dir: &Direction) -> &str {
    match dir {
        Direction::North => "north",
        Direction::South => "south",
        // ERROR: non-exhaustive patterns: `East` and `West` not covered
    }
}

This is a big safety feature. If you add a new variant to an enum later, the compiler tells you every place in your code that needs updating.

The Wildcard Pattern

Use _ to match “everything else”:

fn is_horizontal(dir: &Direction) -> bool {
    match dir {
        Direction::East | Direction::West => true,
        _ => false,
    }
}

The | means “or” — it matches either variant.

Match with Values

Match can also work with integers, strings, and other types:

fn describe_score(score: u32) -> &'static str {
    match score {
        0 => "no points",
        1..=49 => "low",
        50..=89 => "medium",
        90..=100 => "high",
        _ => "invalid",
    }
}

Impl for Enums

Just like structs, enums can have methods:

#[derive(Debug)]
enum Shape {
    Circle(f64),              // radius
    Rectangle(f64, f64),      // width, height
    Triangle(f64, f64, f64),  // three sides
}

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

    fn name(&self) -> &str {
        match self {
            Shape::Circle(_) => "circle",
            Shape::Rectangle(_, _) => "rectangle",
            Shape::Triangle(_, _, _) => "triangle",
        }
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle(5.0),
        Shape::Rectangle(4.0, 6.0),
        Shape::Triangle(3.0, 4.0, 5.0),
    ];

    for shape in &shapes {
        println!("{}: area = {:.2}", shape.name(), shape.area());
    }
}

If Let — Simple Pattern Matching

When you only care about one variant, if let is shorter than match:

fn main() {
    let msg = Message::Echo(String::from("hello"));

    // With match (verbose for one variant)
    match &msg {
        Message::Echo(text) => println!("Got echo: {}", text),
        _ => {}  // Ignore everything else
    }

    // With if let (cleaner)
    if let Message::Echo(text) = &msg {
        println!("Got echo: {}", text);
    }
}

Use match when you handle multiple variants. Use if let when you only care about one.

While Let — Loop Until Pattern Fails

while let loops as long as a pattern matches:

fn main() {
    let mut stack = vec![1, 2, 3, 4, 5];

    while let Some(top) = stack.pop() {
        println!("Popped: {}", top);
    }
    // Prints: 5, 4, 3, 2, 1
}

Vec::pop() returns Option<T> — either Some(value) or None. The loop runs until pop() returns None.

Option Preview

Option is Rust’s most used enum. It represents a value that might or might not exist:

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

Rust has no null. Instead, you use Option:

fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some(String::from("Alex")),
        2 => Some(String::from("Sam")),
        _ => None,
    }
}

fn main() {
    match find_user(1) {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // Or with if let
    if let Some(name) = find_user(2) {
        println!("Found: {}", name);
    }
}

You cannot use an Option<String> as a String directly. You must check for None first. This prevents null pointer crashes at compile time.

Combining Enums with Structs

Enums and structs work well together:

#[derive(Debug)]
enum OrderStatus {
    Pending,
    Shipped(String),    // tracking number
    Delivered,
    Cancelled(String),  // reason
}

#[derive(Debug)]
struct Order {
    id: u32,
    item: String,
    status: OrderStatus,
}

impl Order {
    fn new(id: u32, item: String) -> Order {
        Order {
            id,
            item,
            status: OrderStatus::Pending,
        }
    }

    fn ship(&mut self, tracking: String) {
        self.status = OrderStatus::Shipped(tracking);
    }

    fn describe(&self) -> String {
        let status_text = match &self.status {
            OrderStatus::Pending => String::from("pending"),
            OrderStatus::Shipped(tracking) => format!("shipped ({})", tracking),
            OrderStatus::Delivered => String::from("delivered"),
            OrderStatus::Cancelled(reason) => format!("cancelled: {}", reason),
        };

        format!("Order #{}: {}{}", self.id, self.item, status_text)
    }
}

fn main() {
    let mut order = Order::new(1, String::from("Rust Book"));
    println!("{}", order.describe());

    order.ship(String::from("TRK-12345"));
    println!("{}", order.describe());
}

This pattern — a struct with an enum field for status — is very common in Rust applications.

Summary

ConceptSyntaxPurpose
Enumenum Name { A, B }Type with multiple variants
Data variantsA(String), B { x: i32 }Variants that carry data
matchmatch value { A => ..., B => ... }Exhaustive pattern matching
if letif let Some(x) = opt { ... }Match one pattern
while letwhile let Some(x) = iter { ... }Loop on a pattern
OptionSome(value) / NoneValue that might not exist
Wildcard_Match anything
Or patternA | BMatch multiple patterns

Source Code

View source code on GitHub →

What’s Next?

Now that you know enums and Option, you are ready for error handling. In the next tutorial, we learn Result<T, E>, the ? operator, and how Rust handles errors without exceptions.

Next: Rust Tutorial #8: Error Handling