In the previous tutorial, we learned structs — types where every value has the same fields. But sometimes a value can be one of several different kinds. That is what enums are for.
Enums in Rust are much more powerful than in most languages. In Java or Kotlin, enums are just named constants. In Rust, each variant can hold different data. Combined with pattern matching, enums become one of Rust’s best features.
Defining an Enum
An enum defines a type that can be one of several variants:
enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
print_direction(heading);
}
fn print_direction(dir: Direction) {
match dir {
Direction::North => println!("Going north"),
Direction::South => println!("Going south"),
Direction::East => println!("Going east"),
Direction::West => println!("Going west"),
}
}
Each variant is accessed with EnumName::Variant. Unlike C enums, Rust enum variants are not integers by default — they are proper types.
Enums with Data
Here is where Rust enums get powerful. Each variant can hold different data:
enum Message {
Quit, // No data
Echo(String), // One String
Move { x: i32, y: i32 }, // Named fields (like a struct)
Color(u8, u8, u8), // Three values (like a tuple)
}
One enum, four variants, each with different data. In other languages you would need a class hierarchy or tagged unions for this.
fn main() {
let messages = vec![
Message::Quit,
Message::Echo(String::from("hello")),
Message::Move { x: 10, y: 20 },
Message::Color(255, 0, 128),
];
for msg in &messages {
process_message(msg);
}
}
The Match Expression
match is how you work with enums. It checks which variant a value is and runs the right code:
fn process_message(msg: &Message) {
match msg {
Message::Quit => {
println!("Quit message received");
}
Message::Echo(text) => {
println!("Echo: {}", text);
}
Message::Move { x, y } => {
println!("Move to ({}, {})", x, y);
}
Message::Color(r, g, b) => {
println!("Color: rgb({}, {}, {})", r, g, b);
}
}
}
Each arm of the match destructures the variant. The variables text, x, y, r, g, b are bound to the data inside the variant.
Match Is Exhaustive
You must handle every variant. If you forget one, the compiler gives an error:
fn direction_name(dir: &Direction) -> &str {
match dir {
Direction::North => "north",
Direction::South => "south",
// ERROR: non-exhaustive patterns: `East` and `West` not covered
}
}
This is a big safety feature. If you add a new variant to an enum later, the compiler tells you every place in your code that needs updating.
The Wildcard Pattern
Use _ to match “everything else”:
fn is_horizontal(dir: &Direction) -> bool {
match dir {
Direction::East | Direction::West => true,
_ => false,
}
}
The | means “or” — it matches either variant.
Match with Values
Match can also work with integers, strings, and other types:
fn describe_score(score: u32) -> &'static str {
match score {
0 => "no points",
1..=49 => "low",
50..=89 => "medium",
90..=100 => "high",
_ => "invalid",
}
}
Impl for Enums
Just like structs, enums can have methods:
#[derive(Debug)]
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle(f64, f64, f64), // three sides
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Rectangle(width, height) => width * height,
Shape::Triangle(a, b, c) => {
// Heron's formula
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn name(&self) -> &str {
match self {
Shape::Circle(_) => "circle",
Shape::Rectangle(_, _) => "rectangle",
Shape::Triangle(_, _, _) => "triangle",
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{}: area = {:.2}", shape.name(), shape.area());
}
}
If Let — Simple Pattern Matching
When you only care about one variant, if let is shorter than match:
fn main() {
let msg = Message::Echo(String::from("hello"));
// With match (verbose for one variant)
match &msg {
Message::Echo(text) => println!("Got echo: {}", text),
_ => {} // Ignore everything else
}
// With if let (cleaner)
if let Message::Echo(text) = &msg {
println!("Got echo: {}", text);
}
}
Use match when you handle multiple variants. Use if let when you only care about one.
While Let — Loop Until Pattern Fails
while let loops as long as a pattern matches:
fn main() {
let mut stack = vec![1, 2, 3, 4, 5];
while let Some(top) = stack.pop() {
println!("Popped: {}", top);
}
// Prints: 5, 4, 3, 2, 1
}
Vec::pop() returns Option<T> — either Some(value) or None. The loop runs until pop() returns None.
Option Preview
Option is Rust’s most used enum. It represents a value that might or might not exist:
enum Option<T> {
Some(T),
None,
}
Rust has no null. Instead, you use Option:
fn find_user(id: u32) -> Option<String> {
match id {
1 => Some(String::from("Alex")),
2 => Some(String::from("Sam")),
_ => None,
}
}
fn main() {
match find_user(1) {
Some(name) => println!("Found: {}", name),
None => println!("User not found"),
}
// Or with if let
if let Some(name) = find_user(2) {
println!("Found: {}", name);
}
}
You cannot use an Option<String> as a String directly. You must check for None first. This prevents null pointer crashes at compile time.
Combining Enums with Structs
Enums and structs work well together:
#[derive(Debug)]
enum OrderStatus {
Pending,
Shipped(String), // tracking number
Delivered,
Cancelled(String), // reason
}
#[derive(Debug)]
struct Order {
id: u32,
item: String,
status: OrderStatus,
}
impl Order {
fn new(id: u32, item: String) -> Order {
Order {
id,
item,
status: OrderStatus::Pending,
}
}
fn ship(&mut self, tracking: String) {
self.status = OrderStatus::Shipped(tracking);
}
fn describe(&self) -> String {
let status_text = match &self.status {
OrderStatus::Pending => String::from("pending"),
OrderStatus::Shipped(tracking) => format!("shipped ({})", tracking),
OrderStatus::Delivered => String::from("delivered"),
OrderStatus::Cancelled(reason) => format!("cancelled: {}", reason),
};
format!("Order #{}: {} — {}", self.id, self.item, status_text)
}
}
fn main() {
let mut order = Order::new(1, String::from("Rust Book"));
println!("{}", order.describe());
order.ship(String::from("TRK-12345"));
println!("{}", order.describe());
}
This pattern — a struct with an enum field for status — is very common in Rust applications.
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Enum | enum Name { A, B } | Type with multiple variants |
| Data variants | A(String), B { x: i32 } | Variants that carry data |
| match | match value { A => ..., B => ... } | Exhaustive pattern matching |
| if let | if let Some(x) = opt { ... } | Match one pattern |
| while let | while let Some(x) = iter { ... } | Loop on a pattern |
| Option | Some(value) / None | Value that might not exist |
| Wildcard | _ | Match anything |
| Or pattern | A | B | Match multiple patterns |
Source Code
Related Tutorials
- Rust Tutorial #6: Structs and Methods — the other way to define custom types
- Rust Tutorial #5: Borrowing and References — how &self works in enum methods
- Rust Tutorial #8: Error Handling — coming next
What’s Next?
Now that you know enums and Option, you are ready for error handling. In the next tutorial, we learn Result<T, E>, the ? operator, and how Rust handles errors without exceptions.