Enums & Pattern Matching

Reviewed & published by Brayan K

Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and enums let you model a value that is exactly one of several possibilities, which pattern matching then handles exhaustively.

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 define enums (including variants that carry data), match on them, destructure their contents, and use the concise if let.

What You'll Learn in This Lesson

1️⃣ Defining Enums and Matching

An enum defines a type by listing its possible variants. You then inspect a value with match, which compares it against patterns. A match is an expression (it returns a value) and must be exhaustive — every variant must be handled.

// An enum lists the possible variants a value can be. This one is simple.
enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let heading = Direction::East;

    // match compares a value against patterns. It must be EXHAUSTIVE —
    // you have to cover every variant, or the code won't compile.
    let arrow = match heading {
        Direction::North => "↑",
        Direction::South => "↓",
        Direction::East => "→",
        Direction::West => "←",
    };
    println!("Heading: {}", arrow);   // -> Heading: →
}

If you deleted one arm, the compiler would refuse to build and tell you exactly which variant you forgot. That guarantee is one of Rust's most loved features.

2️⃣ Variants with Data and if let

The real power of enums is that each variant can carry data — tuple-style like Circle(f64) or struct-style with named fields. A match can then destructure that data into variables. When you only care about one variant, if let is a tidy shortcut.

// Enum variants can HOLD DATA — each variant can carry its own values.
enum Shape {
    Circle(f64),              // radius
    Rectangle(f64, f64),      // width, height
    Triangle { base: f64, height: f64 },  // named fields
}

fn area(shape: &Shape) -> f64 {
    // match can DESTRUCTURE the data out of each variant.
    match shape {
        Shape::Circle(r) => 3.14159 * r * r,
        Shape::Rectangle(w, h) => w * h,
        Shape::Triangle { base, height } => 0.5 * base * height,
    }
}

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

    // "if let" handles ONE pattern concisely when you don't need full match.
    let pick = Shape::Circle(1.0);
    if let Shape::Circle(r) = pick {
        println!("a circle of radius {}", r);   // -> a circle of radius 1
    }
}

Notice how each match arm pulled the values out: Circle(r) binds the radius to r, and the triangle arm binds its named fields. The if let handled just the circle case without a full match.

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

// 🎯 YOUR TURN — finish the enum and the match, then run it.
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: &Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => ___,        // 👉 a nickel is worth 5 cents
        Coin::Dime => 10,
        Coin::Quarter => 25,
        // match must be exhaustive — every variant above is covered.
    }
}

fn main() {
    let coins = [Coin::Dime, Coin::Quarter, Coin::Penny];
    let mut total = 0;
    for c in &coins {
        total += value_in_cents(___);   // 👉 pass a reference to c
    }
    println!("Total: {} cents", total);  // expected: Total: 36 cents
}

// ✅ Expected output:
//    Total: 36 cents

Mini-Challenge: A Traffic Light

Model a traffic light as an enum and write functions that transition and describe it. Run it with cargo run.

// 🎯 MINI-CHALLENGE: a traffic light state machine.
//
// 1. Define an enum Light with variants Red, Yellow, Green.
// 2. Write fn next(light: &Light) -> Light that returns the NEXT light:
//      Green -> Yellow, Yellow -> Red, Red -> Green.
//    Use match to cover all three.
// 3. Write fn action(light: &Light) -> &str returning "stop"/"slow"/"go".
// 4. In main, start at Light::Red, print its action, compute next(),
//    print the next light's action.
//
// ✅ Example output:
//    Now: stop
//    Next: go

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Enums & match

GoalRust Syntax
Defineenum E { A, B }
Variant with dataCircle(f64)
Make a valueE::A
Matchmatch v { A => 1, B => 2 }
Catch-all_ => 0
Single patternif let A = v { }

🎉 Lesson Complete!

Practice quiz

What does an enum define in Rust?

  • A list of functions
  • A collection of fields
  • A type that is exactly one of several variants
  • A trait

Answer: A type that is exactly one of several variants. An enum lists the possible variants a value can be; a value is exactly one of them at a time.

How are Rust enums more powerful than enums in many other languages?

  • Each variant can carry its own data
  • They are always integers
  • They cannot be matched
  • They allow null

Answer: Each variant can carry its own data. Rust enum variants can hold data, and different variants can carry different kinds and amounts of data.

What does it mean that a match must be exhaustive?

  • It must have exactly two arms
  • It must use a loop
  • It can only match integers
  • Every possible variant must be handled or the code won't compile

Answer: Every possible variant must be handled or the code won't compile. Rust requires every variant be covered so you can never silently forget a case.

What does the _ arm in a match do?

  • Matches nothing
  • Acts as a wildcard catch-all for anything not already covered
  • Repeats the previous arm
  • Causes a panic

Answer: Acts as a wildcard catch-all for anything not already covered. _ is a wildcard that matches any case not handled by earlier arms, serving as a default.

When is if let preferable to a full match?

  • When you care about only one pattern and ignore the rest
  • When handling many variants
  • When you need exhaustiveness
  • Never

Answer: When you care about only one pattern and ignore the rest. if let is concise for a single pattern, avoiding a match with one arm plus a _ catch-all.

Given the Coin enum where Nickel is 5 and Dime is 10, what total do coins [Dime, Quarter, Penny] sum to in cents?

  • 16
  • 40
  • 36
  • 25

Answer: 36. 10 (Dime) + 25 (Quarter) + 1 (Penny) = 36 cents.

How can a match arm extract data out of a variant like Circle(f64)?

  • By indexing it
  • By destructuring, e.g. Shape::Circle(r) binds r
  • It can't
  • By calling .get()

Answer: By destructuring, e.g. Shape::Circle(r) binds r. match arms destructure variants: Shape::Circle(r) binds the radius to r.

For Shape::Rectangle(w, h) with area w * h, what is the area of Rectangle(3.0, 4.0)?

  • 7.00
  • 6.00
  • 34.00
  • 12.00

Answer: 12.00. w * h = 3.0 * 4.0 = 12.00.

What must be true of every arm of a match used as a value?

  • They must return different types
  • They must return the same type
  • They must each panic
  • They must be empty

Answer: They must return the same type. A match is an expression, so all arms must evaluate to the same type.

What is the downside of using a _ catch-all arm?

  • It is slower
  • It can't compile
  • It silently absorbs new variants you add later
  • It only matches numbers

Answer: It silently absorbs new variants you add later. Because _ swallows everything else, it hides new variants instead of forcing you to handle them.

Continue this course

Frequently asked questions

How are Rust enums more powerful than enums in other languages?

In many languages an enum is just a named list of integer constants. In Rust each variant can also carry its own data, and different variants can carry different kinds and amounts of data. For example, a Shape enum can have Circle(f64) holding a radius and Rectangle(f64, f64) holding two sides. This makes enums a way to model a value that is exactly one of several shapes of data — something other languages need class hierarchies or unions to express.

Why does match have to be exhaustive?

Rust requires a match to handle every possible variant so that you can never forget a case. If you add a new variant to an enum later, every match that does not cover it will fail to compile, pointing you straight to the code that needs updating. This turns a whole class of 'I forgot to handle that case' bugs into compile errors. When you genuinely want to ignore the rest, you add a catch-all arm with _ => ... .

When should I use if let instead of match?

Use if let when you only care about one pattern and want to ignore everything else, because it is more concise than a match with a single arm plus a _ => () catch-all. For example, 'if let Some(x) = maybe { ... }' runs the block only when the value is Some, binding its contents to x. Reach for a full match when you need to handle several variants, since match gives you exhaustiveness checking.

What is the underscore _ arm in a match?

The _ pattern is a wildcard that matches anything not already covered by earlier arms, acting as a default or catch-all. It lets you satisfy exhaustiveness without listing every remaining variant individually. Use it for the 'everything else' case, but be aware it will silently absorb new variants you add later, so prefer explicit arms when you want the compiler to remind you to handle new cases.