Introduction
Rust enums are far more powerful than enums in most other languages. Each variant can hold different types and amounts of data, making them algebraic data types. Combined with match, they give you exhaustive, type-safe control flow that the compiler verifies at compile time.
Key Concepts
- Enum: A type that can be one of several variants. Each variant can optionally hold data (no data, named fields, unnamed fields, or a single value).
Option<T>: Rust's replacement for null. A value is eitherSome(T)orNone, forcing you to handle the absent case.matchexpression: A control flow construct that compares a value against patterns and executes the matching arm. It must be exhaustive — every possible variant must be handled.- Match guard: An additional
ifcondition on a match arm, liken if n < 0 => ....
Real World Context
Enums model state machines, message types, command patterns, and error variants in production Rust. The Result<T, E> enum powers Rust's entire error handling system. HTTP libraries use enums for request methods (GET, POST, PUT). Any time your data has a finite set of possible shapes, an enum is the right tool.
Deep Dive
Rust enum variants can hold different data types:
rustenum Message { Quit, // No data (unit variant) Move { x: i32, y: i32 }, // Named fields (struct variant) Write(String), // Single value (tuple variant) ChangeColor(i32, i32, i32), // Multiple values (tuple variant) }
The Option<T> enum replaces null pointers:
rustlet some_number: Option<i32> = Some(5); let no_number: Option<i32> = None;
You must handle both cases before using the inner value, which prevents null pointer exceptions entirely.
The match expression destructures enum variants and must cover all possibilities:
rustfn describe(msg: Message) { match msg { 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: ({r},{g},{b})"), } }
Match guards add conditional logic to arms:
rustmatch num { n if n < 0 => println!("Negative"), 0 => println!("Zero"), n => println!("Positive: {n}"), }
The _ wildcard catches any remaining cases:
rustmatch value { 1 => println!("One"), _ => println!("Something else"), }
Common Pitfalls
- Forgetting to handle all variants —
matchmust be exhaustive. If you add a new variant to an enum, everymatchon that enum must be updated. Use_as a catch-all only when you genuinely want to ignore remaining variants. - Trying to use
Option<T>values directly — You cannot add anOption<i32>to ani32. You must unwrap or match theOptionfirst. This is by design: it forces you to handle theNonecase.
Best Practices
- Model your domain with enums — When a value can be one of several shapes, use an enum instead of boolean flags or string tags. The compiler will enforce exhaustiveness.
- Prefer
matchover chains ofif/else—matchis more readable and the compiler verifies you have covered all variants.
Summary
- Rust enums can hold data in each variant, making them algebraic data types.
Option<T>replaces null: values are eitherSome(T)orNone.matchmust be exhaustive — every variant must be handled, or a_wildcard used.- Match guards add conditional logic to pattern arms.
- Enums combined with
matchgive you type-safe, compiler-verified control flow.
Code Examples
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five); // Some(6)
let none = plus_one(None); // None