11.7Methods on enums

Last updated June 13, 2026

Everything chapter 10 taught about impl blocks and methods applies to enums, unchanged. You write impl MyEnum { ... }, the methods take &self / &mut self / self exactly as before, and inside them you match on self to do the right thing for each variant. This lesson shows the pattern and then builds the example enums were made for: a state machine.

impl on an enum

Define methods on an enum the same way as on a struct (lesson 10.5):

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

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

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

The only new wrinkle is inside the method: match self. Because the method takes &self, the self in the match is a &TrafficLight, and the arms compare it against each variant. This match self shape is the bread and butter of enum methods, you'll write it constantly. The method groups the per-variant behavior with the type, so callers write the readable light.action() instead of a free function.

Methods that return a new variant

A method can also produce a new value of the enum. The traffic light's "what comes next" is a natural fit, and it's where enums start to feel like machines:

impl TrafficLight {
    fn next(&self) -> TrafficLight {
        match self {
            TrafficLight::Red => TrafficLight::Green,
            TrafficLight::Green => TrafficLight::Yellow,
            TrafficLight::Yellow => TrafficLight::Red,
        }
    }
}

fn main() {
    let mut light = TrafficLight::Red;
    for _ in 0..4 {
        println!("{light:?} -> action: {}", light.action());
        light = light.next();
    }
}
Red -> action: stop
Green -> action: go
Yellow -> action: slow down
Red -> action: stop

next takes &self, reads the current variant, and returns the variant that follows. The loop in main reassigns light to its successor each pass, so the light cycles. This is a state machine: a value that is in exactly one of a fixed set of states, with defined transitions between them. Enums model state machines perfectly, because "exactly one of a fixed set" is precisely what an enum is, and match makes the transition table impossible to write incompletely. Forget a transition and the match in next won't compile (E0004, exhaustiveness from lesson 11.1).

A state machine with data

The traffic light's states carry no data. Real state machines often do, and this is where data-carrying variants (lesson 11.2) and methods combine into something genuinely expressive. Model a simple traffic-light-controlled task, or here, a small download:

#[derive(Debug)]
enum Download {
    Idle,
    InProgress { percent: u8 },
    Done { bytes: u64 },
    Failed(String),
}

impl Download {
    fn status_line(&self) -> String {
        match self {
            Download::Idle => String::from("waiting to start"),
            Download::InProgress { percent } => format!("downloading: {percent}%"),
            Download::Done { bytes } => format!("complete: {bytes} bytes"),
            Download::Failed(reason) => format!("failed: {reason}"),
        }
    }

    fn is_finished(&self) -> bool {
        matches!(self, Download::Done { .. } | Download::Failed(_))
    }
}

fn main() {
    let states = [
        Download::Idle,
        Download::InProgress { percent: 47 },
        Download::Done { bytes: 1024 },
        Download::Failed(String::from("timeout")),
    ];

    for state in &states {
        println!("{} (finished: {})", state.status_line(), state.is_finished());
    }
}
waiting to start (finished: false)
downloading: 47% (finished: false)
complete: 1024 bytes (finished: true)
failed: timeout (finished: true)

Each variant carries the data its state needs: a percent while downloading, a byte count when done, a reason when failed, and nothing when idle. status_line matches and uses each variant's data; is_finished asks a yes/no question about the state, and counts both Done and Failed as finished, which is why the last two lines both read true. The new tool there is matches!, a macro that returns true if the value matches the pattern and false otherwise, a compact shorthand for match x { pattern => true, _ => false }. The | inside it is the multiple-pattern syntax from lesson 11.4.

Key insight

An enum plus an impl block is a small machine: the variants are the states, data-carrying variants attach the data each state needs, and methods that match self are the operations and transitions. Because the enum lists every state and match must cover every variant, the compiler verifies your machine handles all its states, every time you change the set of states. This is "make impossible states unrepresentable" (lesson 3.6) delivered in full.

Quiz time

Question #1

Add a method is_go(&self) -> bool to TrafficLight that returns true only for Green. Write it two ways: with a match, and with matches!.

Show solution
impl TrafficLight {
    fn is_go(&self) -> bool {
        match self {
            TrafficLight::Green => true,
            _ => false,
        }
    }
}

Or, more compactly:

impl TrafficLight {
    fn is_go(&self) -> bool {
        matches!(self, TrafficLight::Green)
    }
}

matches! is the idiomatic choice for a one-pattern yes/no test.

Question #2

Why does the next method's match need to be exhaustive, and what does that buy you if you later add a FlashingRed variant?

Show solution

match must cover every variant (E0004 otherwise). If you add FlashingRed, every match on TrafficLight that doesn't handle it stops compiling, including next and action. The compiler hands you a checklist of exactly the places that need a transition or behavior for the new state, so you can't ship a half-updated state machine.

Question #3

Give the Download enum a method percent_done(&self) -> u8 that returns the percent for InProgress, 100 for Done, and 0 for everything else.

Show solution
impl Download {
    fn percent_done(&self) -> u8 {
        match self {
            Download::InProgress { percent } => *percent,
            Download::Done { .. } => 100,
            _ => 0,
        }
    }
}

The InProgress arm binds percent (a &u8, since self is borrowed) and *percent reads the value out, the dereference from lesson 9.6. Done { .. } ignores the byte count, and the _ covers Idle and Failed.

That's the full enum toolkit: variants, carried data, Option, the depths of match, destructuring, the if let family, and methods. The chapter summary and quiz are next, and then chapter 12 takes the Result type that's been lurking in parse and read_line since chapter 1 and makes error handling something you do on purpose.