Enums in Rust
An introduction to Rust's enum types and pattern matching, covering what they are, why they exist, and how they enable type-safe handling of multiple variants.
An enum is a type that can be exactly one of several explicitly named alternatives. Unlike a struct, where every instance always has all the fields, an enum instance picks a single variant and only carries the data that variant needs. In Rust, enums are not merely integer constants — they are full algebraic data types that can hold structured data inside each variant.
This page gives you the high-level mental model for enums, shows why they matter, and sets the stage for the detailed walkthroughs on defining enums, the match control flow, and the if let syntax.
What Enums Are
An enum (short for enumeration) is a way to create a type whose values can be one variant out of a fixed set. Each variant can optionally carry data — and different variants can hold completely different types of data.
enum ConnectionState {
Disconnected,
Connected { ip: String, port: u16 },
Reconnecting(u32),
}
Here, ConnectionState has three variants:
Disconnectedcarries no data (a unit variant).Connectedcarries two named fields (a struct-like variant).Reconnectingcarries a single unnamed value, here au32representing the number of retry attempts so far (a tuple-like variant).
Any value of type ConnectionState is exactly one of these three — it cannot be “sort of connected” with a phantom retry count. The compiler enforces this at compile time.
Why Enums Exist
Programs constantly deal with situations where data can be one thing or another. A network request can succeed or fail. A character can be alive, knocked out, or dead. An IP address can be version 4 or version 6.
Without enums, a common workaround is to use a struct with a tag field and a bunch of nullable fields:
struct ConnectionStateRaw {
state: String,
ip: Option<String>,
port: Option<u16>,
retries: Option<u32>,
}
This is legal but dangerous. The type system does not prevent setting state = "Disconnected" while leaving retries = Some(3). Every consumer of this struct must read all the Option fields and enforce their own consistency, often with runtime checks and potential panics. Enums solve this by making invalid states literally impossible to construct.
A Core Design Principle:
Rust enums let you make invalid states unrepresentable. When a state cannot be built, no amount of sloppy code can accidentally use it at runtime.
How Enums Work
When you write an enum definition, the compiler creates a discriminated union: a single type whose memory layout can hold the largest variant, plus a hidden tag that tracks which variant is active. You create a value by specifying the variant:
let disconnected = ConnectionState::Disconnected;
let connected = ConnectionState::Connected {
ip: "192.168.1.1".to_string(),
port: 8080,
};
let reconnecting = ConnectionState::Reconnecting(3);
To use the inner data of an enum value, you need to destructure the variant, typically via match, to access its fields. The compiler requires that your match covers every variant — an exhaustive check that prevents forgetting an edge case.
fn describe(state: ConnectionState) {
match state {
ConnectionState::Disconnected => println!("No connection."),
ConnectionState::Connected { ip, port } => {
println!("Connected to {} on port {}", ip, port);
}
ConnectionState::Reconnecting(attempts) => {
println!("Retrying connection (attempt {})", attempts);
}
}
}
If you add a new variant later, every match on that enum will cause a compiler error until you handle the new case. This is not a burden — it’s a safety net that catches missing logic at compile time.
Beginners and Exhaustiveness:
When the compiler says a match is non‑exhaustive, it is protecting you from a real logic bug. Treat the error as a checklist: find every place you match on that enum and decide what the new variant means there.
How Beginners Should Think About Enums
Think of a struct as an AND: a User struct has a name AND an email AND a sign‑in count. An enum is an OR: a Message is a Quit OR a Move OR a Write.
- With a struct, you fill out every field every time.
- With an enum, you choose exactly one variant and provide only the data that variant demands.
Pattern matching then splits the code path based on which variant you have — it’s switch on steroids, but with data extraction built in.
Enums vs Structs
It’s common to wonder when to use an enum instead of multiple structs. A quick test: if the set of possibilities is open‑ended and might grow, a trait‑based solution may be more appropriate. But if the possibilities are known and finite, and especially if they are mutually exclusive states of the same entity, an enum is usually clearer and safer.
| Situation | Choose |
|---|---|
| Fixed set of variants, each carries different data | Enum |
| Every instance has the same fields, always present | Struct |
| Need polymorphic behavior with many implementations | Traits (with trait objects or generics) |
Enums also give you a single type that can be stored in collections, passed to functions, and matched exhaustively — something that scattered structs cannot.
Built‑in Enums: Option and Result
Rust has no null pointer concept. Instead, the standard library provides two enums that every Rust program uses:
enum Option<T> {
None,
Some(T),
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Option<T> represents a value that might be absent. Result<T, E> represents an operation that might succeed with T or fail with E. Because they are plain enums, all the pattern‑matching rules apply — you cannot accidentally use a potentially missing value without handling the None or Err case.
fn maybe_divide(n: f64, d: f64) -> Option<f64> {
if d == 0.0 {
None
} else {
Some(n / d)
}
}
let result = maybe_divide(10.0, 0.0);
match result {
Some(v) => println!("Result: {}", v),
None => println!("Cannot divide by zero"),
}
Don't Skip the Check:
Methods like unwrap() and expect() panic at runtime if the enum is None or Err. They are useful in prototypes and tests, but production code should handle the absent‑value case explicitly.
Pattern Matching and Control Flow
match is the primary way to interact with enums. Every arm must cover a pattern, and the compiler ensures exhaustiveness. If you only care about a single variant, Rust offers a shorter syntax:
if let Some(value) = maybe_divide(10.0, 2.0) {
println!("Got a value: {}", value);
}
if let combines a pattern match with a condition, handling the “happy path” without a verbose match. It’s syntactic sugar for a match where all other arms just do nothing.
Practical Applications
Enums appear everywhere in idiomatic Rust:
- Error handling:
Resultis the backbone of recoverable errors. Functions that can fail returnResult, and callers must decide how to handle failure. - State machines: A server connection can be
Connecting,Connected, orDisconnected, with each state carrying only its relevant data. - Message passing: In GUI frameworks or actor systems, an
EventorMessageenum carries the different kinds of communication between components. - Parsing: Token types in a lexer are often an enum where each variant holds the associated text.
- Tree structures: A binary tree node is naturally an enum: a branch with two subtrees, or a leaf with a value.
Syntax Pitfall: Struct‑Like Variants:
When matching a variant defined with curly braces, you must use curly braces in the pattern, not parentheses. MyEnum::Variant { .. } is correct; MyEnum::Variant(_) will produce a confusing error. The syntax mirrors the definition exactly.
Common Mistakes
- Using the wrong destructuring syntax. If a variant was defined as
Variant { x: i32 }, you writeVariant { x }orVariant { x: some_name }, neverVariant(some_name). The compiler error may mention “expected tuple struct/variant, found struct variant,” which points directly to this mismatch. - Trying to access inner data without matching. Enum fields are not accessible with dot notation. You must destructure the variant first.
- Forgetting to handle new variants after editing an enum. This is a compile‑time error, which is good, but new learners sometimes panic when their previously compiling code breaks. The fix is to go through the compiler errors and add the missing arms.
- Overusing the wildcard
_. A_ => {}arm silences the exhaustiveness error, which means you will never be notified when a new variant is added. Only use_when you genuinely want “all remaining cases do nothing,” and even then, consider whether you’d rather be explicit.
Moving Forward
You now have a working understanding of what enums are and why they are central to Rust.
If you are new to Rust, the single most important idea to carry forward is that enums turn “this can be X or Y” from a comment in the code into an inescapable rule enforced by the compiler. Every line you write that switches on an enum is guaranteed to consider every possibility — that guarantee is the foundation of Rust’s reliability.