Skip to content

Rust Enums

An enum (short for enumeration) defines a type that can be one of a fixed set of possibilities. If a struct is for “this thing has all of these fields,” an enum is for “this thing is exactly one of these options.”

enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("Heading north"),
Direction::South => println!("Heading south"),
Direction::East => println!("Heading east"),
Direction::West => println!("Heading west"),
}
}

Output:

Heading north
▶ Try it Yourself

Notice match here doesn’t need a _ catch-all — because every possible Direction is listed, Rust can already tell the match is complete.

This is where Rust’s enums go further than a typical enum in other languages: each variant can carry its own data:

enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14159 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(3.0);
let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle));
println!("{:.2}", area(&rectangle));
}

Output:

28.27
20.00
▶ Try it Yourself

Shape::Circle carries one f64 (the radius); Shape::Rectangle carries two. match pulls that data straight out for you in each arm — no separate step needed to “unwrap” it.

Just like a struct, an enum can have an impl block with methods. Inside a method, you usually match self to decide what to do for each variant:

#[derive(Debug)]
enum Light {
Red,
Yellow,
Green,
}
impl Light {
fn seconds(&self) -> u32 {
match self {
Light::Red => 30,
Light::Yellow => 3,
Light::Green => 25,
}
}
fn next(&self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{:?} for {} seconds", light, light.seconds());
light = light.next();
}
}

Output:

Red for 30 seconds
Green for 25 seconds
Yellow for 3 seconds
Red for 30 seconds
▶ Try it Yourself

next returns a brand new Light, and light = light.next(); swaps the old one out. That’s why light is declared mut. #[derive(Debug)] works on enums the same way it does on structs, so {:?} prints the variant’s name.

Putting the logic in methods keeps it next to the type. Anyone using a Light can call light.seconds() without knowing how the timing is decided. And if you add a new variant later, like FlashingYellow, the compiler points at every match you need to update.

Option: an enum you’ve already been using

Section titled “Option: an enum you’ve already been using”

The Option type you’ll meet properly on its own page is just an enum defined in the standard library:

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

Once enums click, Option stops looking like special syntax — it’s a completely ordinary enum with two variants, one of which happens to carry a value.

Why not just use a struct with a “type” field?

Section titled “Why not just use a struct with a “type” field?”

You could imagine faking this with a struct and a string field like "circle" or "rectangle" — but then nothing stops you from typing "circl" by mistake, and you’d need to manually check which fields are actually relevant for each type. An enum makes invalid combinations impossible to write in the first place, and the compiler forces you to handle every variant.

Make an enum Coin with the variants Penny, Nickel, Dime and Quarter. Write fn value_in_cents(coin: &Coin) -> u32, then add up a purse of coins: a quarter, a dime, a nickel, a penny and another quarter. Hint: store the coins in an array and loop over &purse.

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,
}
}
fn main() {
let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0;
for coin in &purse {
total += value_in_cents(coin);
}
println!("Total: {total} cents");
}

Output:

Total: 66 cents
▶ Try it Yourself

Take the Shape enum from this page and add a Triangle variant that holds the lengths of its three sides. Then write a perimeter function (the distance all the way around) that works for every shape. Hint: a circle’s perimeter is 2.0 * 3.14159 * radius. A triangle’s is its three sides added up.

Show solution
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn perimeter(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 2.0 * 3.14159 * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:.2}", perimeter(shape));
}
}

Output:

6.28
18.00
12.00
▶ Try it Yourself

Try deleting the Shape::Triangle arm from the match. Rust refuses to compile, because the match no longer covers every variant. That’s the compiler making sure you never forget a case.

Make an enum Temperature with two variants that each hold an f64: Celsius and Fahrenheit. Add a method to_celsius(&self) -> f64, and print Celsius(25.0), Fahrenheit(212.0) and Fahrenheit(50.0) in °C with one decimal place. Hint: to convert, use (f - 32.0) * 5.0 / 9.0.

Show solution
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
impl Temperature {
fn to_celsius(&self) -> f64 {
match self {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
fn main() {
let readings = [
Temperature::Celsius(25.0),
Temperature::Fahrenheit(212.0),
Temperature::Fahrenheit(50.0),
];
for t in &readings {
println!("{:.1} °C", t.to_celsius());
}
}

Output:

25.0 °C
100.0 °C
10.0 °C
▶ Try it Yourself

Because self is borrowed, c and f are references (&f64). The math works on references directly, but to return the plain Celsius value you write *c, which means “the number c points to”.

  • An enum defines a type as one of several named variants: enum Direction { North, South, ... }.
  • Variants can carry their own data, and different variants can carry different data.
  • match naturally destructures enum data and requires every variant to be handled.
  • Enums can have methods in an impl block, just like structs.
  • Option (and Result, covered later) are just enums from the standard library — nothing magic about them.

Quick check

1. What can each variant of a Rust enum do, unlike a typical enum in other languages?

2. Why doesn't the Direction match example need a catch-all _ arm?

3. What is Option<T> actually implemented as in the standard library?

Score: 0 / 3