How to Iterate Over Enum Variants in Rust

Iterate over Rust enum variants by manually listing them in a collection or using external crates like strum for automatic generation.

When the compiler won't list your variants

You're building a settings screen. A dropdown needs to list every possible Theme variant so the user can pick one. You write a loop to iterate over Theme, expecting Rust to hand you Light, Dark, and HighContrast in order. The compiler rejects you immediately. Rust enums are not arrays. The compiler doesn't keep a runtime list of your variants. You have to tell it how to walk through them.

This trips up developers coming from languages with reflection. In those languages, you can ask the type system for a list of members at runtime. Rust has no reflection. The compiler uses enums to enforce logic and memory layout, then discards the catalog to save space. If you need to iterate, you must provide the list or generate it.

Enums are types, not lists

Think of an enum like a set of distinct shapes. The compiler enforces that you only pass a Circle or a Square. It doesn't keep a catalog of shapes in memory. When you ask for "all shapes," the compiler shrugs. It optimized the catalog away.

Rust enums are discriminants. A Color enum is often just a byte. 0 for Red, 1 for Blue. The compiler maps the names to numbers. Once the code is compiled, the names are gone. There's no string "Red" in the binary unless you print it. Iteration requires a sequence. The compiler doesn't generate a sequence. It generates a type. You need to provide the sequence.

This design keeps Rust fast and small. No metadata tables. No runtime introspection overhead. You pay for what you use. If you want iteration, you write the iteration logic. The compiler ensures your logic is correct.

The manual array approach

The simplest way to iterate is to write the list yourself. You create an array of all variants and loop over it. This gives you explicit control over order and requires no dependencies.

#[derive(Debug, Clone, Copy)]
enum Suit {
    Hearts,
    Diamonds,
    Clubs,
    Spades,
}

fn main() {
    // Explicit array defines the iteration order.
    let suits = [Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit::Spades];

    for suit in suits {
        println!("{:?}", suit);
    }
}

The Clone and Copy derives are essential here. If you omit Copy, the loop moves the value out of the array on the first iteration. The second iteration tries to use a moved value. The compiler stops you with E0382 (use of moved value). With Copy, the value duplicates implicitly. The loop runs safely.

This approach works well for small, stable enums. You control the order. You see the list right there. If you add a variant, you must remember to add it to the array. Forgetting to update the array is a common bug. The compiler won't catch it because the array is just data. You need a way to keep them in sync.

Automating with strum

For larger enums or public APIs, manual arrays become maintenance headaches. The strum crate solves this with macros. A macro is code that writes code. strum inspects your enum at compile time and generates the iterator for you.

// Add strum to Cargo.toml: strum = "0.26"
use strum::{EnumIter, IntoEnumIterator};

#[derive(Debug, EnumIter, Clone, Copy)]
enum Planet {
    Mercury,
    Venus,
    Earth,
    Mars,
}

fn main() {
    // strum generates the iterator automatically.
    for planet in Planet::iter() {
        println!("Visiting {:?}", planet);
    }
}

The EnumIter derive generates a function Planet::iter() that returns an iterator over all variants. The order matches the declaration order in the source code. If you add Jupiter, the iterator updates automatically. You can't forget a variant. The macro keeps the list in sync with the enum definition.

The performance is identical to a manual array. strum generates a static array and iterates it. There is no runtime cost. No reflection. No allocation. You get convenience without sacrificing speed.

Convention aside: strum and serialization

When you iterate enums, you often want to display them as strings. strum integrates with Display to handle this. You can derive Display and use #[strum(serialize = "...")] to control the string representation.

use strum::{EnumIter, Display};

#[derive(Debug, EnumIter, Display, Clone, Copy)]
enum Status {
    #[strum(serialize = "active")]
    Active,
    #[strum(serialize = "inactive")]
    Inactive,
}

fn main() {
    for status in Status::iter() {
        // Prints "active", then "inactive".
        println!("{}", status);
    }
}

This pattern is standard for UIs and APIs. You iterate the variants and print the user-friendly string. strum handles the mapping. You avoid manual match statements for formatting.

Skipping variants

Sometimes a variant shouldn't appear in iteration. An internal Debug mode or a deprecated option might need to be hidden. strum supports skipping variants with #[strum(skip)].

use strum::EnumIter;

#[derive(Debug, EnumIter, Clone, Copy)]
enum Mode {
    Public,
    Private,
    #[strum(skip)]
    Debug,
}

fn main() {
    // Yields Public and Private. Debug is skipped.
    for mode in Mode::iter() {
        println!("{:?}", mode);
    }
}

The macro filters out skipped variants during generation. The iterator only yields the allowed variants. This keeps your iteration logic clean. You don't need to filter manually in the loop.

Pitfalls and errors

Iteration works best for unit variants. Enums with data-bearing variants cause trouble. You can't iterate over Message::Text(String) because every instance has different text. You iterate over the shape, not the content.

If you try to build an array of data-bearing variants, you must provide dummy data. This is messy and breaks abstraction.

enum Message {
    Greet(String),
    Quit,
}

fn main() {
    // This fails. Greet requires a String.
    // let msgs = [Message::Greet, Message::Quit];
    
    // You have to fake the data.
    let msgs = [Message::Greet("".to_string()), Message::Quit];
    
    for msg in msgs {
        println!("{:?}", msg);
    }
}

The compiler rejects the first attempt with E0425 (cannot find value) or type errors. Greet is a constructor, not a value. You must call it with arguments. If you fake the data, you're iterating instances, not variants. The dummy string is arbitrary. It might not represent a valid state.

If you need to iterate variants with data, step back. You probably want a struct with a vector. Or separate the variant list from the data. Use a MessageKind enum for iteration and a Message struct for data.

Non-exhaustive enums

If you mark an enum #[non_exhaustive], you're telling the compiler that more variants might appear later. strum refuses to derive EnumIter for non-exhaustive enums. It can't generate a complete list. External crates might add variants that strum doesn't know about.

#[derive(strum::EnumIter)] // Error: cannot derive EnumIter for non_exhaustive enum
#[non_exhaustive]
enum ApiVersion {
    V1,
    V2,
}

The compiler rejects this. You must iterate manually or remove non_exhaustive. If the enum is truly non-exhaustive, iteration is unsafe. You can't guarantee you have all variants. Redesign the API to avoid iteration over non-exhaustive types.

Convention aside: Clone vs Copy

If you're iterating an enum, derive Copy. Iteration implies reuse. If you have to clone in a loop, you're allocating heap data repeatedly. That's slow. Check if Copy is possible. If the enum holds heap data, ask why you're iterating it. You might be better off iterating a list of IDs or indices.

Decision matrix

Use a manual array when the enum is small and stable. You write the list once. You get zero dependencies. You have explicit control over order. The code is transparent. Anyone reading it sees the variants immediately.

Use strum when the enum grows or changes often. The macro generates the iterator automatically. You add a variant, and the iterator updates without touching the list. The runtime cost is identical to a manual array. You avoid copy-paste errors.

Use a custom trait when you cannot add dependencies and the enum is complex. You implement fn variants() -> &'static [Self] manually. This is verbose but gives you full control over the logic. You can add filtering or grouping inside the trait.

Reach for match when you need to handle logic per variant. Iteration is for listing or transforming. If you're doing different work for each case, match is the right tool. Don't iterate just to match. Match directly.

Where to go next