11.1Enums

Last updated June 13, 2026

Chapter 10 built types whose parts coexist: a Customer has a name and an email and a balance, all at once. This chapter builds the other kind, where a value is exactly one of several alternatives, never more than one. That type is called an enum, and in Rust it's not the minor housekeeping feature it is in many languages. It's half of how you model data.

The problem with using a bool

Suppose a program tracks a traffic light. Your first instinct might be a bool:

let is_green = true;

But a traffic light isn't a yes/no question. It's red, yellow, or green: three states, and a bool only has two. You could bolt on a second bool (is_red, is_yellow), but now nothing stops is_red and is_green from both being true, which is a state that can't physically exist. You've made an impossible situation representable, and somewhere down the line a bug will create it.

What you want is a type whose only possible values are the three real states. That's an enum:

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

TrafficLight is now a type, like Customer was. Its values, the things after the brace, are called variants. A value of type TrafficLight is always exactly one of Red, Yellow, or Green, and the compiler guarantees there is no fourth option and no way to be two at once. The impossible state is now unrepresentable, which is the phrase lesson 3.6 promised this chapter would deliver on.

Creating and using an enum value

Name a variant with the enum, a ::, and the variant: TrafficLight::Red. The :: is the same path separator you used for associated functions in lesson 10.6, and it reads the same way: "the Red that belongs to TrafficLight."

#[derive(Debug)]
enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn main() {
    let light = TrafficLight::Green;
    println!("{:?}", light);
}
Green

Enums take #[derive(Debug)] exactly like structs (lesson 10.7), and it prints the variant name. The variable light has type TrafficLight; the compiler infers it from the value, just as it inferred i32 for let x = 5; back in lesson 4.9. You could annotate it let light: TrafficLight = ... but you rarely need to.

Enums pair with match

You've already met the tool for acting on an enum: match, from lesson 7.3. Back then match worked on integers and characters and you were promised it would come into its own later. This is later. Matching on an enum is the natural way to do something different for each variant:

fn action(light: &TrafficLight) -> &str {
    match light {
        TrafficLight::Red => "stop",
        TrafficLight::Yellow => "slow down",
        TrafficLight::Green => "go",
    }
}

fn main() {
    let light = TrafficLight::Green;
    println!("{}", action(&light));
}
go

Here's where the design pays off. Remember the exhaustiveness beat from lesson 7.3: a match must cover every possibility. With an enum, the compiler knows exactly what every possibility is, because you listed them. Delete the Green arm and the program is refused:

error[E0004]: non-exhaustive patterns: `TrafficLight::Green` not covered
 --> src/main.rs:8:11
  |
8 |     match light {
  |           ^^^^^ pattern `TrafficLight::Green` not covered
  |
note: `TrafficLight` defined here
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms

The compiler tells you precisely which variant you forgot. This is the feature that makes enums worth reaching for: when you later add a fourth variant, say FlashingRed, every match that doesn't handle it stops compiling, and the compiler hands you a checklist of every place in your program that needs updating. A bool-and-if version would just silently do the wrong thing. With an enum, "handle the new case everywhere" becomes a job the compiler assigns you, not one you have to remember.

Key insight

Reach for an enum whenever a value has a fixed, known set of states, especially if a bool (or two) is tempting but doesn't quite fit. The win isn't just readability. An enum plus match makes the compiler verify you've handled every case, and re-verify it every time the set of cases changes. "Make impossible states unrepresentable" is the slogan; exhaustiveness checking is the mechanism.

C-style enums and underlying values

The enums above are sometimes called C-style: plain named variants with no extra data attached. By default each variant is just a distinct label, but you can also give them integer values, which is occasionally useful when an enum mirrors numbers from the outside world:

enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    Teapot = 418,
}

fn main() {
    println!("{}", HttpStatus::NotFound as u32);
}
404

The as u32 is the explicit cast from lesson 4.10: an enum variant can be turned into its integer value with as, but not automatically, and the reverse (number back to variant) isn't a cast at all, because an arbitrary number might not be a valid variant. You'll reach for this rarely. Most Rust enums never touch numbers, because the next lesson reveals what variants can carry instead, and it's far more interesting than integers.

Quiz time

Question #1

Why is a TrafficLight enum a better model than two booleans is_red and is_green?

Show solution

The enum can only ever be exactly one of Red, Yellow, Green. Two booleans allow nonsensical combinations like is_red = true, is_green = true (both on) or both false (off), states a real light can't be in. The enum makes those impossible states unrepresentable, so they can't become bugs.

Question #2

This is refused. What's the error, and what is the compiler protecting you from?

enum Direction { North, South, East, West }

fn label(d: &Direction) -> &str {
    match d {
        Direction::North => "N",
        Direction::South => "S",
        Direction::East => "E",
    }
}
Show solution

E0004: non-exhaustive patterns, Direction::West not covered. The compiler knows Direction has exactly four variants and you handled three, so it refuses the incomplete match. It's protecting you from forgetting a case, and it would do the same the day you add a fifth direction. Add a Direction::West => "W" arm (or a _ wildcard, though naming every variant is usually better, so a future variant forces you to revisit this code).

Question #3

Write an enum Coin with variants Penny, Nickel, Dime, Quarter, and a function value_in_cents(coin: &Coin) -> u32 that returns 1, 5, 10, or 25 using match.

Show solution
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: &Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

All four variants are covered, so the match is exhaustive and compiles. This is the Rust Book's coin example; the next lesson upgrades it by letting a variant carry data.

These enums carry only their identity. The next lesson lets variants carry values, which is the feature that turns enums from a tidy list into one of Rust's most powerful ideas.