Real data is messy. A user might be active, suspended, or pending approval. A shape might be a circle, rectangle, or triangle. A result might be success with data, or an error.
Rust's enums let you express "this could be one of several things." Combined with pattern matching, you get one of the most powerful features in any programming language.
Basic Enums
An enum lists possible variants:
enum Direction {
Up,
Down,
Left,
Right,
}
Each variant is a constant. Create one:
let dir = Direction::Up;
Enums are perfect for modeling states:
enum Status {
Active,
Suspended,
PendingApproval,
}
struct User {
username: String,
status: Status,
}
let user = User {
username: String::from("alice"),
status: Status::Active,
};
Enums with Data
Each variant can carry its own data:
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Named fields like a struct
Write(String), // Single value
ChangeColor(u8, u8, u8), // Tuple-like
}
This is incredibly flexible. You can model almost any "one-of" situation:
let msg = Message::Move { x: 10, y: 20 };
let note = Message::Write(String::from("Hello!"));
The Option Enum — Rust's Null Alternative
This is the most important enum in Rust. It's so important it's built into the language:
enum Option<T> {
Some(T), // We have a value
None, // We don't have a value
}
Every value that might "not exist" uses Option. No null. No billion-dollar mistakes:
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("alice"))
} else {
None
}
}
let user = find_user(1); // Some("alice")
let missing = find_user(99); // None
You must handle both cases. Rust won't let you ignore None.
Pattern Matching with match
match is like a super-powered switch statement:
let status = Status::Active;
match status {
Status::Active => println!("User is active!"),
Status::Suspended => println!("User is suspended"),
Status::PendingApproval => println!("Awaiting approval"),
}
The compiler ensures you handle every possible variant. Add a new variant? You'll get a compiler error until you handle it.
Matching with Option
This is where match shines:
fn greet_user(id: u32) {
match find_user(id) {
Some(name) => println!("Hello, {}!", name),
None => println!("User not found"),
}
}
Capturing Values
Extract the data inside variants:
let msg = Message::Write(String::from("Hi!"));
match msg {
Message::Quit => println!("Goodbye"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text: {}", text),
Message::ChangeColor(r, g, b) => println!("RGB: {}, {}, {}", r, g, b),
}
The _ Catch-All
Handle "anything else" concisely:
let dir = Direction::Up;
match dir {
Direction::Up => println!("Going up!"),
Direction::Down => println!("Going down!"),
_ => println!("Moving sideways"), // Covers Left and Right
}
The _ Placeholder (Ignoring Values)
Need a value but don't care what it is? Use _:
let msg = Message::Write(String::from("Hello"));
match msg {
Message::Write(_) => println!("Got a message!"), // Don't care about content
_ => println!("Other message"),
}
if let — When You Only Care About One Case
Sometimes you only want to handle one variant:
let msg = Message::Write(String::from("Hi!"));
// This is cleaner than full match when you only care about one case
if let Message::Write(text) = msg {
println!("Message: {}", text);
}
This is especially useful with Option:
let user = find_user(1);
// Instead of:
// match user {
// Some(name) => println!("{}", name),
// None => (),
// }
// You can write:
if let Some(name) = user {
println!("Found: {}", name);
}
Enum Methods
Just like structs, enums can have methods:
enum Status {
Active,
Suspended,
PendingApproval,
}
impl Status {
fn is_active(&self) -> bool {
matches!(self, Status::Active)
}
}
let status = Status::Active;
if status.is_active() {
println!("User is active!");
}
What's Next
Enums and pattern matching let you model real-world complexity cleanly. Next up: Error Handling — making failures explicit instead of pretending everything always works.