RustEnums

Enums in Rust

Enums in Rust are far more powerful than enums in C or Java. In Rust, each variant of an enum can carry its own data — different types, different shapes. This makes enums the right tool for modeling values that can be one of several distinct things, each potentially holding different information.

Basic Enums

A basic enum lists a set of named variants. You define it with the enum keyword:

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

let heading = Direction::North;

match heading {
    Direction::North => println!("Going north"),
    Direction::South => println!("Going south"),
    Direction::East  => println!("Going east"),
    Direction::West  => println!("Going west"),
}
Note
Enum variants are namespaced under the enum type. You write `Direction::North`, not just `North`. This avoids naming collisions across different enums.
Enums with Data

Each variant can hold data — and different variants can hold completely different types of data. This is what makes Rust enums special:

RUST
enum Message {
    Quit,                        // no data
    Move { x: i32, y: i32 },    // named fields (like a struct)
    Write(String),               // one String value
    ChangeColor(i32, i32, i32),  // three i32 values
}

let m1 = Message::Quit;
let m2 = Message::Move { x: 10, y: 20 };
let m3 = Message::Write(String::from("hello"));
let m4 = Message::ChangeColor(255, 128, 0);

Think of each variant as its own mini-type. Message::Move is like an anonymous struct. Message::Write is like a tuple struct. Message::Quit is like a unit struct. All of them share the single type Message.

Enums Can Hold Any Type

Enum variants can hold strings, integers, tuples, structs, Vecs, Boxes — any Rust type at all:

RUST
#[derive(Debug)]
struct Point { x: f64, y: f64 }

#[derive(Debug)]
enum Shape {
    Circle { center: Point, radius: f64 },
    Rectangle { top_left: Point, bottom_right: Point },
    Triangle(Point, Point, Point),
    Label(String),
}

let s1 = Shape::Circle {
    center: Point { x: 0.0, y: 0.0 },
    radius: 5.0,
};

let s2 = Shape::Label(String::from("origin"));

println!("{:?}", s1);
println!("{:?}", s2);
impl Blocks on Enums

Just like structs, enums can have methods defined in an impl block:

RUST
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        match self {
            Message::Quit => println!("Quitting"),
            Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
            Message::Write(text) => println!("Writing: {}", text),
            Message::ChangeColor(r, g, b) => {
                println!("Color: rgb({}, {}, {})", r, g, b)
            }
        }
    }

    fn is_quit(&self) -> bool {
        matches!(self, Message::Quit)
    }
}

let msg = Message::Write(String::from("hello world"));
msg.call();
println!("Is quit: {}", msg.is_quit());
Pattern Matching with match

The match expression is the primary way to work with enums. It must be exhaustive — every possible variant must be handled:

RUST
#[derive(Debug)]
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String),  // Quarter holds a state name
}

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

let q = Coin::Quarter(String::from("Alaska"));
println!("{} cents", value_in_cents(&q));
Tip
The `_` wildcard pattern matches any value and is commonly used as a catch-all arm at the end of a `match` block when you do not need to handle every variant explicitly.
The Option Enum

Rust has no null. Instead, the standard library provides Option<T>, which forces you to explicitly handle the absence of a value:

RUST
// Defined in the standard library (no need to import)
// enum Option<T> {
//     None,
//     Some(T),
// }

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

let result = find_user(1);

match result {
    Some(name) => println!("Found: {}", name),
    None       => println!("User not found"),
}

Common Option methods that avoid verbose match blocks:

RUST
let some_val: Option<i32> = Some(42);
let none_val: Option<i32> = None;

// unwrap_or: provide a default
println!("{}", some_val.unwrap_or(0));  // 42
println!("{}", none_val.unwrap_or(0));  // 0

// map: transform the inner value if Some
let doubled = some_val.map(|v| v * 2);
println!("{:?}", doubled);  // Some(84)

// is_some / is_none
println!("has value: {}", some_val.is_some());  // true
println!("is empty:  {}", none_val.is_none());  // true
Warning
Avoid `unwrap()` in production code — it panics if the value is `None`. Use `unwrap_or`, `unwrap_or_else`, `?`, or an explicit `match` instead.
The Result Enum

Result<T, E> is Rust's error-handling type. Instead of throwing exceptions, functions that can fail return Result. The caller is forced to deal with errors:

RUST
// Defined in the standard library
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }

use std::num::ParseIntError;

fn parse_age(s: &str) -> Result<u32, ParseIntError> {
    let n: u32 = s.trim().parse()?;
    Ok(n)
}

match parse_age("25") {
    Ok(age)  => println!("Age is {}", age),
    Err(e)   => println!("Parse error: {}", e),
}

match parse_age("abc") {
    Ok(age)  => println!("Age is {}", age),
    Err(e)   => println!("Parse error: {}", e),
}

Common Result methods:

RUST
let ok:  Result<i32, &str> = Ok(10);
let err: Result<i32, &str> = Err("oops");

println!("{}", ok.unwrap_or(0));          // 10
println!("{}", err.unwrap_or(0));         // 0
println!("{:?}", ok.map(|v| v * 2));      // Ok(20)
println!("{}", ok.is_ok());               // true
println!("{}", err.is_err());             // true
Deriving Traits on Enums

Like structs, enums can derive common traits:

RUST
#[derive(Debug, Clone, PartialEq)]
enum Status {
    Active,
    Inactive,
    Pending(String),
}

let s1 = Status::Active;
let s2 = Status::Active;
let s3 = Status::Pending(String::from("review"));

println!("{:?}", s1);
println!("s1 == s2: {}", s1 == s2);
println!("s1 == s3: {}", s1 == s3);

let s4 = s3.clone();
println!("Cloned: {:?}", s4);
Enums as State Machines

Enums are a natural fit for modeling state machines. Each state is a variant, and transitions are methods that consume the old state and produce the new one:

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

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

    fn duration_seconds(&self) -> u32 {
        match self {
            TrafficLight::Red    => 60,
            TrafficLight::Green  => 45,
            TrafficLight::Yellow => 5,
        }
    }
}

let mut light = TrafficLight::Red;
for _ in 0..4 {
    println!("{:?} — {} seconds", light, light.duration_seconds());
    light = light.next();
}
Enums vs Constants

Feature

Enum

Constant

Type safety

Enforced — variants are distinct types

No — just a value of a primitive type

Data attachment

Each variant can carry data

Constants hold a single fixed value

Exhaustiveness

match forces you to handle all variants

No such guarantee

Pattern matching

Full match / if let / while let support

Only equality checks

Documentation

Variants are self-documenting

Need comments for intent

Real-World Example: Payment Method

RUST
#[derive(Debug, Clone)]
enum PaymentMethod {
    Cash,
    CreditCard {
        number: String,
        expiry: String,
    },
    PayPal(String),          // holds email address
    Crypto { coin: String, wallet: String },
}

impl PaymentMethod {
    fn display_name(&self) -> String {
        match self {
            PaymentMethod::Cash => String::from("Cash"),
            PaymentMethod::CreditCard { number, .. } => {
                let last4 = &number[number.len().saturating_sub(4)..];
                format!("Card ending in {}", last4)
            }
            PaymentMethod::PayPal(email) => {
                format!("PayPal ({})", email)
            }
            PaymentMethod::Crypto { coin, .. } => {
                format!("{} wallet", coin)
            }
        }
    }

    fn requires_online(&self) -> bool {
        match self {
            PaymentMethod::Cash => false,
            _ => true,
        }
    }
}

fn process_payment(amount: f64, method: &PaymentMethod) {
    println!(
        "Processing ${:.2} via {} (online: {})",
        amount,
        method.display_name(),
        method.requires_online()
    );
}

let methods = vec![
    PaymentMethod::Cash,
    PaymentMethod::CreditCard {
        number: String::from("4111111111111234"),
        expiry: String::from("12/27"),
    },
    PaymentMethod::PayPal(String::from("user@example.com")),
];

for method in &methods {
    process_payment(99.99, method);
}
Real-World Example: HTTP Method

RUST
#[derive(Debug, Clone, PartialEq)]
enum HttpMethod {
    Get,
    Post,
    Put,
    Patch,
    Delete,
    Head,
    Options,
}

impl HttpMethod {
    fn is_safe(&self) -> bool {
        matches!(self, HttpMethod::Get | HttpMethod::Head | HttpMethod::Options)
    }

    fn is_idempotent(&self) -> bool {
        matches!(
            self,
            HttpMethod::Get
                | HttpMethod::Put
                | HttpMethod::Delete
                | HttpMethod::Head
                | HttpMethod::Options
        )
    }
}

let methods = [
    HttpMethod::Get,
    HttpMethod::Post,
    HttpMethod::Put,
    HttpMethod::Delete,
];

for m in &methods {
    println!(
        "{:?} — safe: {}, idempotent: {}",
        m,
        m.is_safe(),
        m.is_idempotent()
    );
}
if let — Concise Single-Variant Matching

When you only care about one variant, if let is more concise than a full match:

RUST
let config: Option<String> = Some(String::from("debug"));

// Verbose with match
match &config {
    Some(val) => println!("Config: {}", val),
    None => {},
}

// Concise with if let
if let Some(val) = &config {
    println!("Config: {}", val);
}

// Also works with Result
let result: Result<i32, &str> = Ok(42);
if let Ok(n) = result {
    println!("Got: {}", n);
}
Enums as Algebraic Data Types

Rust enums are what functional programmers call sum types (also known as tagged unions or algebraic data types). A value of an enum type is exactly one of its variants at any time — the compiler tracks which one and refuses to compile code that ignores a variant.

  • Structs are product types — they hold ALL their fields simultaneously

  • Enums are sum types — they hold exactly ONE variant at a time

  • Together they let you model any domain accurately

  • The compiler's exhaustiveness check means you can't forget a case

  • This is why Rust has no NullPointerException class of bugs

Success
You now understand Rust enums from basic variants to data-carrying variants, `Option`, `Result`, state machines, and pattern matching. Enums paired with structs and `match` form the backbone of idiomatic Rust programs.