Enums and Pattern Matching
Rust enums can carry data per variant, making them far more powerful than enums in most other languages.
2 min read
An enum in Rust defines a type by listing its possible variants. That much is familiar from other languages — but Rust enums can attach different data to each variant, which is what makes them genuinely powerful.
A basic enum
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
}Variants that carry data
Instead of pairing an enum with a separate struct to hold associated data, each variant can carry its own:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}V4 and V6 are both IpAddr, but they carry different shapes of data — something a plain enum plus a separate struct can't express as directly. Variants can even hold named fields, like a struct:
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields
Write(String), // single value
ChangeColor(i32, i32, i32), // tuple of values
}Matching on an enum
match, from an earlier lesson, is how you handle each variant — and the compiler requires every variant to be covered:
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!("color: {r}, {g}, {b}"),
}
}Add a new variant to Message later, and every match on it that lacks a _ catch-all will fail to compile until it's updated — the compiler tracking down every place that needs to change, instead of you discovering a missed case at runtime.
if let for a single case
When you only care about one variant and want to ignore the rest, match with a _ arm can feel heavier than necessary. if let handles that case concisely:
fn main() {
let config_max = Some(3u8);
// Equivalent match:
// match config_max {
// Some(max) => println!("max is {max}"),
// _ => (),
// }
if let Some(max) = config_max {
println!("max is {max}");
}
}if let trades exhaustiveness checking for brevity — reach for it when ignoring the other cases is genuinely fine, and reach for match when you want the compiler to make sure you've thought about all of them.
Option, a real-world enum
Rust has no null. Instead, the standard library defines Option<T> as an ordinary enum with two variants, Some(T) and None:
enum Option<T> {
Some(T),
None,
}This isn't special syntax — it's the same enum mechanism from this lesson, applied to the single most common problem in programming: representing the possible absence of a value. The next section covers Option (and its cousin Result) in depth, but it's worth recognizing now that they're built from exactly the tool you just learned.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.