Enums and Pattern Matching
Master enums to represent variants, pattern matching with match expressions, and exhaustiveness checking. Understand algebraic data types and how Rust's enums are more powerful than in other languages.
A traffic light can be red, yellow, or green. An HTTP response can succeed or fail. A file operation might produce data or an error. These situations represent a choice among distinct possibilities. Structs bundle multiple values together—product types. Enums represent alternatives—sum types. One variant or another, never both simultaneously.
Pattern matching extracts data from enums and ensures you handle all cases. The compiler checks exhaustiveness—if you forget a case, the code won't compile. This eliminates the bugs that occur in other languages when you forget to check a condition or handle a return value.
Defining Enums
An enum defines a type that can be one of several variants:
enum TrafficLight {
Red,
Yellow,
Green,
}
The enum TrafficLight has three variants. A value of type TrafficLight must be one of these three—never more, never less.
Create instances by specifying the variant:
let light = TrafficLight::Red;
The :: syntax accesses the variant. Each variant is namespaced under the enum name.
Enums with Data
Variants can contain data:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
The Quit variant has no data. Move has named fields like a struct. Write has a single String. ChangeColor has three u8 values. Each variant can have different types and amounts of data.
Create instances by providing the necessary data:
let quit = Message::Quit;
let move_msg = Message::Move { x: 10, y: 20 };
let write_msg = Message::Write(String::from("Hello"));
let color_msg = Message::ChangeColor(255, 0, 0);
Enums are more expressive than multiple structs because they group related alternatives under one type. A function accepting Message can receive any variant. The type system ensures all variants are handled.
Pattern Matching with match
The match expression destructures enums and executes code based on the variant:
fn process(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!("Changing color to RGB({}, {}, {})", r, g, b);
}
}
}
Each arm of the match handles one variant. The pattern binds the variant's data to variables (x, y, text, r, g, b), which the arm can use.
The match expression must be exhaustive—every possible variant must have an arm. If you remove an arm, the compiler rejects the code. This prevents bugs where you forget to handle a case.
Match is an Expression
The match expression returns a value:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Each arm returns a u8. The match expression's value becomes the function's return value. No return keyword needed.
All arms must return the same type:
let result = match coin {
Coin::Penny => 1,
Coin::Nickel => "five", // Error: mismatched types
_ => 10,
};
The compiler rejects this because one arm returns an integer and another returns a string.
Patterns Bind Values
Patterns extract data from variants:
enum UsState {
Alabama,
Alaska,
// ...
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}", state);
25
}
}
}
The Quarter variant contains a UsState. The pattern Coin::Quarter(state) binds the state to the variable state, which the arm uses.
Matching Option<T>
The Option<T> enum represents optional values—something or nothing:
enum Option<T> {
Some(T),
None,
}
You've seen Option earlier as Rust's alternative to null. Pattern matching extracts values from Option:
fn describe(opt: Option<i32>) {
match opt {
Some(value) => println!("Got: {}", value),
None => println!("Got nothing"),
}
}
The pattern Some(value) binds the contained value. The pattern None handles the absence of a value.
Forgetting the None case causes a compilation error:
match opt {
Some(value) => println!("{}", value),
// Error: non-exhaustive patterns: `None` not covered
}
The compiler ensures you handle both cases. This eliminates null pointer exceptions—you can't access a value that might not exist without explicitly checking.
The _ Placeholder
When you don't care about certain variants, use the _ placeholder:
let number = Some(7);
match number {
Some(7) => println!("Lucky seven!"),
_ => println!("Some other number or nothing"),
}
The _ matches anything. It's often used as the last arm to handle all remaining cases.
The _ can also ignore parts of a pattern:
match color {
Message::ChangeColor(r, _, _) => println!("Red: {}", r),
_ => (),
}
This extracts only the red component, ignoring green and blue.
if let: Concise Pattern Matching
When you care about only one variant, if let provides shorter syntax than match:
let some_value = Some(3);
if let Some(x) = some_value {
println!("Got: {}", x);
}
This is equivalent to:
match some_value {
Some(x) => println!("Got: {}", x),
_ => (),
}
The if let syntax checks if the pattern matches. If it does, it binds the data and executes the block. If it doesn't, nothing happens.
You can add an else clause for the non-matching case:
if let Some(x) = some_value {
println!("Got: {}", x);
} else {
println!("Got nothing");
}
The if let syntax sacrifices exhaustiveness checking for conciseness. Use it when you want to handle one specific case and ignore others.
while let: Conditional Loops
The while let construct loops while a pattern matches:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
The loop continues while stack.pop() returns Some. When it returns None, the loop exits.
Patterns in Function Parameters
Function parameters can use patterns:
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("({}, {})", x, y);
}
let point = (3, 5);
print_coordinates(&point);
The pattern &(x, y) destructures the tuple reference, binding x and y.
Matching Literals and Ranges
Patterns can match literal values:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("something else"),
}
Patterns can match ranges:
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
The ..= syntax creates an inclusive range. This works with integers and characters.
Matching Multiple Patterns
The | operator matches multiple patterns:
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("something else"),
}
Destructuring Structs and Enums
Patterns destructure structs:
struct Point {
x: i32,
y: i32,
}
let origin = Point { x: 0, y: 0 };
match origin {
Point { x, y } => println!("({}, {})", x, y),
}
You can match specific values while binding others:
match origin {
Point { x: 0, y } => println!("On the y-axis at {}", y),
Point { x, y: 0 } => println!("On the x-axis at {}", x),
Point { x, y } => println!("Somewhere else: ({}, {})", x, y),
}
Patterns destructure nested structures:
enum Color {
Rgb(u8, u8, u8),
Hsv(u8, u8, u8),
}
enum Message {
ChangeColor(Color),
}
let msg = Message::ChangeColor(Color::Rgb(255, 0, 0));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("RGB: {}, {}, {}", r, g, b);
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("HSV: {}, {}, {}", h, s, v);
}
}
The pattern Message::ChangeColor(Color::Rgb(r, g, b)) matches a ChangeColor containing an Rgb color and binds the component values.
Match Guards
A match guard adds an additional condition to a pattern:
let number = Some(4);
match number {
Some(x) if x < 5 => println!("Less than five: {}", x),
Some(x) => println!("Five or greater: {}", x),
None => println!("No number"),
}
The if x < 5 is a match guard. The arm executes only if the pattern matches AND the guard condition is true.
Guards enable expressing conditions that patterns alone can't:
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
The guard applies to the entire pattern 4 | 5 | 6, not just the last value.
@ Bindings
The @ operator lets you bind a value while also testing it:
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_var @ 3..=7 } => {
println!("Found an id in range: {}", id_var);
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range");
}
Message::Hello { id } => {
println!("Found some other id: {}", id);
}
}
The pattern id: id_var @ 3..=7 tests if id is in the range 3 to 7 and binds it to id_var. Without @, you can test the range or bind the value, but not both.
Why Enums and Pattern Matching Matter
Enums represent choices explicitly. Instead of using integers or strings with meaning attached by convention, you define an enum where each variant has semantic meaning. The type system ensures you use values correctly.
Pattern matching forces exhaustiveness. When you add a new variant to an enum, the compiler identifies every match expression that needs updating. This prevents bugs where new cases aren't handled.
Together, enums and pattern matching provide algebraic data types with compile-time guarantees. Languages that lack sum types resort to workarounds: null values, sentinel values, or exception handling. These approaches hide failure modes and allow bugs to escape. Rust makes failure modes explicit and ensures you handle them.
The Option and Result types, both enums, form the foundation of Rust's approach to optional values and error handling. Understanding enums and pattern matching is essential for writing idiomatic Rust code. Master these concepts and you'll appreciate why Rust's error handling feels natural rather than burdensome.
