Examples of Using Enums in Rust (With Practical Code Snippets)

Enums (short for enumerations) are one of Rust’s most powerful and distinctive features. They let you define a type that can represent a fixed set of variants, each of which can optionally carry data. Combined with pattern matching, enums help you write expressive, type-safe code with fewer runtime errors. In this guide, you’ll explore several practical examples of using enums in Rust, ranging from simple state modeling (like traffic lights) to more realistic use cases such as network responses, configuration options, and error handling. You’ll also see how enums interact with `match`, `if let`, methods, and generics. By the end, you’ll not only understand how to declare and use enums, but also when they are preferable to alternatives like strings, integers, or inheritance-based designs in other languages. You’ll walk away with copy‑paste‑ready Rust code snippets and a deeper understanding of how enums fit into idiomatic Rust programming.
Written by
Jamie

Understanding Enums in Rust

Enums in Rust define a type by listing all of its possible variants. Unlike many languages where enums are just named integers, Rust enums are algebraic data types: each variant can store additional data, potentially of different types.

At a high level, enums help you:

  • Represent a closed set of states (e.g., Red, Yellow, Green).
  • Bundle data with each state (e.g., an HTTP status code with a response body).
  • Leverage exhaustive pattern matching, so the compiler ensures you handle every case.
  • Eliminate many classes of bugs that would otherwise be caught only at runtime.

According to the Rust language team, enums and pattern matching are considered core to Rust’s design because they enable safer, more maintainable code than traditional error codes or flag-based APIs. You can read more in the Rust Book’s section on enums.

Important Note
In Rust, enums are first-class types. They can implement traits, have methods, be generic, and be used anywhere you would use a struct or primitive type.


Example 1: Simple Traffic Light Enum

Use Case

Modeling a finite set of states is a classic use case for enums. A traffic light has three states: Red, Yellow, and Green. Using an enum makes the code self-documenting and prevents invalid values.

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn describe_light(light: TrafficLight) {
    match light {
        TrafficLight::Red => println!("Stop: the light is red."),
        TrafficLight::Yellow => println!("Prepare: the light is yellow."),
        TrafficLight::Green => println!("Go: the light is green."),
    }
}

fn main() {
    let current = TrafficLight::Green;
    describe_light(current);
}

Why This Is Better Than Strings or Integers

Compare this to using a String or an integer to represent the light:

  • With a String, you might accidentally pass "Grenn" or "Blue".
  • With an integer, the meaning of 0, 1, or 2 isn’t obvious to readers.

With an enum:

  • The compiler enforces that the value is one of the defined variants.
  • match ensures you handle all cases (or explicitly use _ for a catch-all).

Pro Tip
When you add a new variant (e.g., FlashingRed), the compiler will show you all the match expressions that need updating. This is a major maintainability advantage in large codebases.


Example 2: Enum with Data (Chat Messages)

Use Case

Enums can also carry data, making them ideal for representing different kinds of messages, each with its own payload. Consider a chat application with text, image, and video messages.

enum ChatMessage {
    Text(String),
    Image { url: String, width: u32, height: u32 },
    Video { url: String, duration_secs: u32 },
}

fn print_message(msg: ChatMessage) {
    match msg {
        ChatMessage::Text(content) => {
            println!("Text: {}", content);
        }
        ChatMessage::Image { url, width, height } => {
            println!("Image: {} ({}x{} px)", url, width, height);
        }
        ChatMessage::Video { url, duration_secs } => {
            println!("Video: {} ({} seconds)", url, duration_secs);
        }
    }
}

fn main() {
    let text = ChatMessage::Text("Hello, world!".to_string());
    let image = ChatMessage::Image {
        url: "https://example.com/pic.png".to_string(),
        width: 800,
        height: 600,
    };

    print_message(text);
    print_message(image);
}

Key Benefits

  • Each variant can carry different fields and different types.
  • You avoid parallel structures like multiple optional fields in a single struct.
  • Pattern matching makes it easy to branch logic by variant.

Important Note
Struct-like enum variants (e.g., Image { url, width, height }) improve clarity, especially when there are multiple fields. Use them whenever the data has a clear structure.


Example 3: Enum for File Operations

Use Case

File-related actions often have the same shape (they operate on a path) but represent different intentions: open, save, delete, etc. An enum can group these actions into a single type.

enum FileAction {
    Open(String),
    Save { path: String, overwrite: bool },
    Delete(String),
}

fn handle_file_action(action: FileAction) {
    match action {
        FileAction::Open(path) => {
            println!("Opening file: {}", path);
            // here you might call std::fs::File::open(path)
        }
        FileAction::Save { path, overwrite } => {
            if overwrite {
                println!("Saving (overwriting) file: {}", path);
            } else {
                println!("Saving file if it does not exist: {}", path);
            }
        }
        FileAction::Delete(path) => {
            println!("Deleting file: {}", path);
        }
    }
}

fn main() {
    let action = FileAction::Save {
        path: "document.txt".to_string(),
        overwrite: true,
    };
    handle_file_action(action);
}

Why This Pattern Scales

As your application grows, you can extend FileAction with variants like:

  • Rename { from: String, to: String }
  • Copy { from: String, to: String }

The compiler will remind you to update all match expressions, keeping logic consistent across your codebase.

Pro Tip
Use enums for command-like APIs where each command has a small, well-defined set of parameters. This often leads to clearer and more testable code than using strings or raw flags.


Example 4: Enums for Result and Option (Error Handling)

Use Case

Rust’s standard library heavily relies on enums for error handling and optional values:

  • Option<T>: represents Some(T) or None.
  • Result<T, E>: represents Ok(T) or Err(E).

Both are enums defined in the standard library.

// Simplified versions of the real definitions:

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

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 2.0) {
        Ok(result) => println!("Result: {}", result),
        Err(err) => eprintln!("Error: {}", err),
    }
}

Why This Matters

According to the Rust 2024 developer survey, Rust is widely adopted in systems programming and safety-critical domains partly because of its strong compile-time guarantees and error-handling patterns based on enums and pattern matching. Enums like Result and Option force you to explicitly handle failure and absence, reducing runtime surprises.

Important Note
When working with Result and Option, idiomatic Rust often uses methods like map, and_then, and the ? operator instead of explicit match expressions. These methods are possible because enums can implement traits and methods just like structs.

For more on Rust’s error handling philosophy, see the Rust Book chapter on error handling.


Example 5: Enums for Network Responses

Use Case

Network operations often return different kinds of responses: success with data, redirects, or errors. An enum can model these outcomes in a type-safe way.

enum HttpResponse {
    Success { status: u16, body: String },
    Redirect { status: u16, location: String },
    ClientError { status: u16, message: String },
    ServerError { status: u16, message: String },
}

fn handle_response(resp: HttpResponse) {
    match resp {
        HttpResponse::Success { status, body } => {
            println!("Success ({}): {}", status, body);
        }
        HttpResponse::Redirect { status, location } => {
            println!("Redirect ({}): follow {}", status, location);
        }
        HttpResponse::ClientError { status, message } => {
            eprintln!("Client error ({}): {}", status, message);
        }
        HttpResponse::ServerError { status, message } => {
            eprintln!("Server error ({}): {}", status, message);
        }
    }
}

fn main() {
    let ok = HttpResponse::Success {
        status: 200,
        body: "OK".to_string(),
    };

    let not_found = HttpResponse::ClientError {
        status: 404,
        message: "Not Found".to_string(),
    };

    handle_response(ok);
    handle_response(not_found);
}

Real-World Insight

In production Rust web services (for example, those built with frameworks like Actix Web or Axum), it’s common to use enums to represent:

  • High-level API errors (e.g., AuthError, ValidationError, DatabaseError).
  • Application states (e.g., UserState::Anonymous, UserState::Authenticated(UserInfo)).

These enums are often combined with traits like std::error::Error for integration with logging and observability systems.

Pro Tip
When modeling HTTP or protocol responses, prefer struct-like variants with named fields. This keeps your code readable and resilient to field order changes.


Example 6: Method Implementations on Enums

Use Case

Enums can have impl blocks, just like structs. This lets you attach behavior directly to the enum, keeping related logic together.

Consider a user’s access level in an application:

enum AccessLevel {
    Guest,
    User,
    Admin,
}

impl AccessLevel {
    fn can_delete_posts(&self) => bool {
        matches!(self, AccessLevel::Admin)
    }

    fn can_create_posts(&self) -> bool {
        !matches!(self, AccessLevel::Guest)
    }
}

fn main() {
    let level = AccessLevel::User;

    println!("Can create posts? {}", level.can_create_posts());
    println!("Can delete posts? {}", level.can_delete_posts());
}

Why Attach Methods to Enums?

  • Encapsulates behavior with the data representation.
  • Avoids scattering match logic across the codebase.
  • Makes the API easier to discover via IDE auto-completion.

Important Note
Use methods on enums for common operations that depend on the variant. For one-off transformations, a local match expression is usually clearer.


Example 7: Using if let and matches! with Enums

Use Case

Sometimes you only care about one variant and want concise syntax. Rust provides if let and the matches! macro for this.

enum JobStatus {
    Pending,
    Running(u32), // progress percentage
    Completed,
    Failed(String),
}

fn log_status(status: &JobStatus) {
    if let JobStatus::Running(progress) = status {
        println!("Job is running: {}% complete", progress);
    }

    if matches!(status, JobStatus::Failed(_)) {
        eprintln!("Job failed; check logs for details.");
    }
}

fn main() {
    let status = JobStatus::Running(42);
    log_status(&status);
}

When to Use These Patterns

  • Use if let when you want to extract data from a single variant.
  • Use matches! when you only need a boolean check.

These patterns are common in asynchronous Rust code (e.g., when polling futures or checking state machines) where concise, readable checks are important.

Pro Tip
Don’t overuse if let chains. If you’re checking multiple variants or doing substantial work in each branch, a full match is usually clearer and ensures exhaustiveness.


Performance and Memory Considerations

Rust enums are implemented efficiently. Under the hood, an enum is typically represented as:

  • A discriminant (tag) indicating which variant it is.
  • The data for that variant.

The size of an enum is roughly the size of its largest variant plus the size of the discriminant. The Rust compiler may optimize representation depending on the specific enum (for example, using niche optimization for Option<&T>).

For detailed technical information, see the Rust reference on data layout.

Important Note
In most application-level code, you don’t need to micro-optimize enum layout. Focus on clear modeling first; only investigate layout if profiling shows a bottleneck.


Best Practices for Using Enums in Rust

  1. Use enums for closed sets of variants
    If your type represents a fixed, known set of possibilities, an enum is usually the right choice.

  2. Prefer enums over “stringly-typed” code
    Avoid using String or &str to represent states or modes when a finite set of options exists.

  3. Attach methods for common behaviors
    Implement methods on your enums to centralize logic and reduce repeated match blocks.

  4. Combine enums with traits
    Implement traits like Display, Error, or custom traits on your enums to integrate with the rest of your codebase.

  5. Leverage the compiler
    When you add a new variant, let the compiler guide you to all the places that need updating. This is especially valuable in large systems.


FAQ: Enums in Rust

1. How are Rust enums different from C-style enums?

C-style enums are usually just named integer constants. Rust enums are algebraic data types: each variant can carry data, and Rust enforces exhaustive pattern matching. This makes them more expressive and safer than plain integer-based enums.

2. When should I use an enum vs. a struct?

Use a struct when you have one shape of data with multiple fields that always appear together. Use an enum when you have multiple alternative shapes (variants), each with its own data. If you find yourself adding many Option<T> fields to a struct, an enum is often a better fit.

3. Can enums be generic in Rust?

Yes. Enums can be generic over one or more type parameters. Option<T> and Result<T, E> are standard examples. You can define your own generic enums, such as enum ApiResponse<T> { Success(T), Error(String) }.

4. Are enums expensive in terms of performance?

In most cases, no. Enums are compiled down to efficient representations similar to tagged unions in C. The overhead is typically just the discriminant plus the largest variant’s size. For performance-critical code, you can inspect layout using tools like std::mem::size_of::<T>() and the Rust reference on type layout.

5. Can I convert between enums and integers or strings?

Yes, but it’s not automatic. You can implement From, TryFrom, or custom methods to convert between enums and integers/strings. For textual conversions (e.g., parsing configuration), it’s common to implement FromStr or use libraries like serde for serialization and deserialization.


Enums are central to idiomatic Rust. By using them to model states, messages, errors, and configuration options, you gain stronger compile-time guarantees, clearer code, and easier refactoring. The examples above should give you a solid starting point for applying enums effectively in your own Rust projects.

Explore More Rust Code Snippets

Discover more examples and insights in this category.

View All Rust Code Snippets