Chapter 29: Rust Enums
Rust enums — one of the most powerful and most loved features in Rust.
Many people call enums the secret weapon of Rust. They let you model choices, states, variants, tagged unions, and sum types in a way that is completely safe, expressive, and prevents entire classes of bugs at compile time.
Let me explain Rust enums like your personal teacher sitting next to you with code open: slowly, step by step, with lots of small-to-real-world examples you can copy-paste and run right now, Hyderabad-flavored analogies (traffic signals, biryani orders, phone battery states), comparisons to other languages, and building from very simple → medium → advanced patterns.
1. What is an Enum? (The Core Idea)
An enum (short for enumeration) lets you define a type that can be one of several possible variants.
- Each variant is a named choice
- Variants can carry different kinds of data (different types, different amounts)
- At any moment, an enum value holds exactly one variant (not multiple, not none)
- Rust forces you to handle all possible variants (exhaustive matching)
Analogy: Think of ordering biryani at Paradise. You can order:
- Chicken Biryani
- Mutton Biryani
- Veg Biryani
- Special Haleem (with extra garnish)
Each choice is different — some have chicken, some mutton, some veggies, some have extra data (garnish level). An enum lets you say: “This order is exactly one of these things — and you must handle every possibility.”
2. Defining a Simple Enum (No Data)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
enum TrafficLight { Red, Yellow, Green, } fn main() { let signal = TrafficLight::Red; // Match must handle ALL variants match signal { TrafficLight::Red => println!("Stop! 🔴"), TrafficLight::Yellow => println!("Slow down... 🟡"), TrafficLight::Green => println!("Go! 🟢"), } } |
- No data → just names (like C-style enum)
- :: to access variants (namespaced under the enum)
- match forces you to cover every case → no forgotten states
3. Enums with Data (The Real Power)
Variants can hold data — any type, any amount.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
enum Message { Quit, // no data Move { x: i32, y: i32 }, // named fields (struct-like) Write(String), // tuple-like ChangeColor(i32, i32, i32), // tuple of 3 i32 } fn main() { let msg1 = Message::Write(String::from("Namaste Hyderabad!")); let msg2 = Message::Move { x: 10, y: -5 }; let msg3 = Message::ChangeColor(255, 128, 0); // Match and destructure match msg1 { Message::Quit => println!("Quit"), Message::Move { x, y } => println!("Move to ({}, {})", x, y), Message::Write(text) => println!("Message: {}", text), Message::ChangeColor(r, g, b) => println!("Color: rgb({},{},{})", r, g, b), } } |
- Each variant can have different payload
- Destructuring in match binds variables automatically
- Compiler error if you forget any variant
4. The Famous Option<T> Enum (Built-in, Extremely Common)
Rust has no null — instead it uses Option<T>:
|
0 1 2 3 4 5 6 7 8 9 |
enum Option<T> { None, Some(T), } |
(It’s in the prelude — you don’t need to write it.)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
fn main() { let favorite_food: Option<String> = Some(String::from("Biryani")); let no_food: Option<String> = None; // Safe handling match favorite_food { Some(food) => println!("I love {}!", food), None => println!("No favorite food today"), } // Shorter: if let if let Some(food) = no_food { println!("Got: {}", food); } else { println!("Nothing"); } // unwrap_or, unwrap_or_else, etc. let food = favorite_food.unwrap_or(String::from("Idli")); println!("Default: {}", food); } |
→ Prevents billions of null pointer bugs — compiler forces you to handle None
5. The Famous Result<T, E> Enum (Error Handling)
|
0 1 2 3 4 5 6 7 8 9 |
enum Result<T, E> { Ok(T), Err(E), } |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use std::fs::File; fn main() { let file_result = File::open("config.toml"); match file_result { Ok(file) => println!("File opened successfully!"), Err(error) => println!("Failed: {}", error), } // Better: ? operator (in functions returning Result) // let file = File::open("config.toml")?; } |
→ Most Rust code uses Result for errors — no exceptions
6. Enums with Methods (via impl)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
enum IpAddrKind { V4(u8, u8, u8, u8), V6(String), } impl IpAddrKind { fn print(&self) { match self { IpAddrKind::V4(a, b, c, d) => println!("IPv4: {}.{}.{}.{}", a, b, c, d), IpAddrKind::V6(addr) => println!("IPv6: {}", addr), } } } fn main() { let home = IpAddrKind::V4(127, 0, 0, 1); let loopback = IpAddrKind::V6(String::from("::1")); home.print(); loopback.print(); } |
7. Common Patterns & Tips (2026 Style)
- Use enums for state machines (GameState::Playing, GameState::Paused…)
- Use enums for commands (Message enum above)
- Use Option for maybe present values
- Use Result for operations that can fail
- Prefer if let / while let when you only care about one variant
- match must be exhaustive — _ wildcard catches remaining cases
Practice Challenge for Today
Create cargo new rust_enums and try:
- enum FoodOrder { ChickenBiryani(u8), MuttonBiryani(u8), VegBiryani, SpecialHaleem { spice_level: u8, extra_garnish: bool } }
- Write match that prints description + price based on variant
- Add method is_spicy(&self) -> bool
- Create function that returns Option<FoodOrder> based on input string
- Use Result for a fake file-reading function
Want next?
- Pattern matching deep dive (match, if let, while let, @ bindings)?
- Enums vs structs — when to use which?
- Enums with generics (like Option<T>, Result<T, E>)?
- Or traits on enums/structs (next big topic)?
Just tell me — your Rust teacher from Hyderabad is right here! 🦀🚀
