How to use enum

Define an enum with the `enum` keyword and handle its variants using a `match` expression to ensure all cases are covered.

The shape of your data

You are building a command parser for a terminal tool. The user can type quit, move 10 20, or color 255 0 0. In JavaScript, you might represent this as an object with a type string and optional fields. In Python, you might use a dictionary or a base class with subclasses. Rust gives you enum. It looks like a simple list at first glance, but it is actually a type system feature that lets you pack different shapes of data into a single variable while guaranteeing the compiler knows exactly what is inside at every step.

Think of an enum as a multi-tool. A standard struct is like a fixed toolbox: every slot has a specific purpose, and the layout never changes. An enum is a single pocket that can hold exactly one tool at a time. The pocket itself has a fixed size, but the tool inside changes depending on what you just picked up. When you declare an enum, you are telling the compiler every possible shape the data can take. The compiler then reserves enough space to hold the largest variant, plus a small tag to track which variant is currently active.

This design eliminates a whole class of runtime errors. You never have to check if a field exists before reading it. You never have to parse a string to figure out what kind of object you are holding. The type system enforces the contract at compile time.

How the compiler tracks variants

Under the hood, Rust represents an enum as a tagged union. The compiler allocates memory for the largest variant and adds a discriminant, usually a single byte, to identify which variant is active. When you create a value, the compiler writes the discriminant and places the variant data in the allocated space. When you read the value, the compiler checks the discriminant and routes execution to the correct branch.

/// Represents commands a user can send to a terminal interface.
enum Message {
    /// The user wants to exit the program.
    Quit,
    /// The user wants to move a cursor or object.
    Move { x: i32, y: i32 },
    /// The user wants to send a text payload.
    Write(String),
    /// The user wants to change the background color.
    ChangeColor(u8, u8, u8),
}

fn main() {
    // Create a unit variant. No extra data is attached.
    let msg = Message::Quit;

    // Pattern match exhaustively. The compiler forces you to handle every variant.
    match msg {
        Message::Quit => println!("Shutting down"),
        Message::Move { x, y } => println!("Moving to coordinates {}, {}", x, y),
        Message::Write(text) => println!("Processing text: {}", text),
        Message::ChangeColor(r, g, b) => println!("Setting RGB to {}, {}, {}", r, g, b),
    }
}

The match expression is where the enum shines. It does not just compare values. It destructures them. When you write Message::Move { x, y }, you are telling the compiler to extract the fields and bind them to new variables. The compiler verifies that every possible variant is handled. If you add a new variant later, the code stops compiling until you update every match block. This is not a suggestion. It is a hard guarantee.

Convention aside: always derive Debug on your enums during development. Add #[derive(Debug)] above the enum keyword. It costs nothing and saves hours when you need to print a value during debugging. The community treats it as standard practice for any custom type.

Do not treat match as a glorified if/else chain. Use it to decompose data. Trust the exhaustiveness check. It will catch your missing cases before they reach production.

Carrying data and calculating with it

Enums become powerful when you attach methods to them. You can define behavior that changes based on the active variant, keeping the logic close to the data. This pattern replaces virtual dispatch in many cases and lets the compiler inline the branches for maximum performance.

/// A simple 2D shape that can calculate its own area.
#[derive(Debug, Clone)]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

/// Calculate the area based on the active variant.
fn area(shape: &Shape) -> f64 {
    // Borrow the enum instead of taking ownership.
    // This lets the caller reuse the shape after the call.
    match shape {
        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { width, height } => width * height,
        Shape::Triangle { base, height } => 0.5 * base * height,
    }
}

fn main() {
    // Create a shape on the stack.
    let rect = Shape::Rectangle { width: 10.0, height: 5.0 };
    
    // Pass a reference to avoid moving the value.
    let a = area(&rect);
    println!("Area: {}", a);
    
    // The original value is still valid and usable.
    println!("Original shape: {:?}", rect);
}

Notice the &Shape parameter. When you pass an enum by value, the entire tagged union moves into the function. If you only need to read the data, pass a reference. The match block then works with borrowed fields. The compiler automatically adjusts the types of radius, width, and height to &f64 inside the arms. You can dereference them directly in arithmetic operations, and Rust handles the coercion.

Convention aside: prefer tuple variants for simple positional data and struct variants when the fields have semantic names. ChangeColor(u8, u8, u8) is fine for raw RGB values. Move { x: i32, y: i32 } is clearer when the coordinates represent spatial positions. The compiler does not care, but your future self will.

Keep your enum methods focused on the data they own. If the logic grows beyond a few lines, extract it into a separate function that takes the enum as a parameter. Do not turn your enum into a catch-all namespace.

When the compiler catches you

The exhaustiveness check is strict, but it can trip you up if you are not careful with ownership and references. The most common friction point is trying to use a value after matching it by value.

enum Status {
    Active,
    Inactive,
}

fn check_status(s: Status) {
    // Matching by value moves `s` into the match arms.
    match s {
        Status::Active => println!("Running"),
        Status::Inactive => println!("Stopped"),
    }
    
    // This line will fail to compile.
    // The compiler rejects it with E0382 (use of moved value).
    println!("Status was: {:?}", s);
}

The compiler moved s into the match expression. Once the match completes, the value is gone. If you need to inspect the enum after the match, borrow it first. Change match s to match &s. The arms will then work with references, and the original value remains intact.

Another frequent error is mismatched types when constructing variants. If you write Message::ChangeColor(255, 0, 0) but the enum expects u16, the compiler stops you with E0308 (mismatched types). The error message points directly to the offending literal and tells you the expected type. Fix the literal or add an explicit cast like 255u16.

Convention aside: mark public enums with #[non_exhaustive] if you plan to add variants in future releases. This tells downstream crates that their match blocks must include a catch-all _ arm. It protects your API from breaking changes when you extend the enum later.

Treat the exhaustiveness check as a feature, not a hurdle. Write your match blocks to handle every case explicitly. Use _ only when you genuinely do not care about a variant, and document why.

Choosing the right container

Use enums when a value can be one of several distinct shapes, and you need the compiler to enforce handling every possibility. Use enums when you want to attach behavior that varies by variant without introducing null checks or runtime type identification. Use enums when you are modeling states, commands, AST nodes, or protocol messages where the structure changes based on context.

Reach for structs when the data always has the same layout, even if some fields are optional. Structs are faster to access because there is no discriminant to check. They are also easier to serialize when the schema is fixed.

Pick a plain integer or string when the variants are truly static and you only need to compare equality. Enums add compile-time guarantees, but they also add memory overhead for the discriminant and padding. If you are packing thousands of values into a tight array and memory is the bottleneck, a raw integer with a mapping function might be more efficient.

Counter-intuitive but true: the more variants you add, the more valuable the enum becomes. The compiler's exhaustiveness check turns future maintenance into a guided tour instead of a minefield.

Where to go next