Enums & Pattern Matching
Rust enums let each variant carry its own data, and match forces you to handle every single one before your code will even compile.
In many languages, an 'enum' is just a fancy integer: Color { RED, GREEN, BLUE } compiles down to 0, 1, 2, and every variant has exactly the same shape — none. Rust's enums are a different, far more powerful idea: each variant of an enum can carry its own data, of its own shape, and the compiler tracks which shape you're holding at every point in your program. A Message type can have a Quit variant with nothing attached, a Move { x: i32, y: i32 } variant that behaves like an inline struct, a Write(String) variant that wraps a single value, and a ChangeColor(i32, i32, i32) variant that wraps three — all under one type, and all handled through the same tool.
Defining an enum lists its variants inside enum Name { ... }, separated by commas, and each variant can be a plain name, a tuple-like list of types, or a struct-like list of named fields — you're not limited to one style across the whole enum. This is what makes a Rust enum a true 'sum type': a Message value is exactly one of Quit, Move, Write, or ChangeColor at any given moment, never more than one and never a partial mix, and the memory Rust allocates for it is sized to fit whichever variant is largest. Compare that to a struct, which holds all of its fields at once — enums and structs are the two complementary ways to combine types, 'one of these' versus 'all of these.'
The tool built to consume an enum is match, and its defining feature is exhaustiveness: the compiler requires every arm to cover every possible variant (and, for types like integers, every possible value) before your code is allowed to compile. Forget a variant, and you get a compile error — 'non-exhaustive patterns' — not a runtime surprise three months later when someone adds a fifth Message variant and one call site quietly never handles it. This is the single biggest reason Rust's enums outclass a C-style switch on an int constant: a switch can fall through, can silently ignore a case, and gives the compiler nothing to check against, while match turns 'did I handle every case' from a code-review question into a build-time guarantee.
Inside a match arm, the pattern doesn't just check which variant you have — it destructures the data out of it in the same step. Message::Move { x, y } => ... binds x and y directly from the struct-like variant; Message::Write(text) => ... binds text from the tuple-like one. Patterns can also match on plain values, ranges (4..=9), multiple options at once (1 | 2 | 3), and add an extra condition with a guard (n if n < 0); when you genuinely don't care about the remaining cases, the _ wildcard arm satisfies exhaustiveness by catching everything not already matched above it. The one rule to internalize is order: arms are checked top to bottom, and the first pattern that matches wins, so a _ (or any other broad pattern) placed too early will silently steal cases you meant to handle further down.
Writing a full match just to handle one interesting variant and ignore the rest is common enough that Rust gives you a shortcut: if let Some(max) = config_max { ... } runs the block only when the pattern matches, with an optional else for everything else, and no _ arm required. It's exactly equivalent to a match with one real arm and a _ => {} arm, just without the ceremony — reach for if let when you truly only care about a single case, and reach back for match the moment you need to react differently to more than one.
The same idea extended into a loop is while let: while let Some(top) = stack.pop() { ... } keeps running the body for as long as the pattern keeps matching, and stops the instant it doesn't — here, the moment pop() returns None because the stack is empty. It's the idiomatic way to drain a collection or consume a stream of Option/Result values one at a time without writing a loop { match ... { break } } by hand.
Between them, enums and match give Rust a way to model 'this or that, and nothing else' with a precision most mainstream languages can't check at compile time — and that precision becomes essential in the next lesson, where Option and Result turn out to be nothing more than enums you've already learned to handle.
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),}impl Message {fn call(&self) {match self {Message::Quit => println!("Quit: no data attached"),Message::Move { x, y } => println!("Move to ({}, {})", x, y),Message::Write(text) => println!("Write: {}", text),Message::ChangeColor(r, g, b) => println!("Change color to ({}, {}, {})", r, g, b),}}}fn main() {let messages = vec![Message::Quit,Message::Move { x: 10, y: 20 },Message::Write(String::from("hello")),Message::ChangeColor(255, 0, 0),];for msg in &messages {msg.call();}}
An enum whose variants carry completely different shapes of data — nothing, named fields, a single value, or three values — matched exhaustively inside its own impl block.
fn describe_number(n: i32) -> &'static str {match n {0 => "zero",1 | 2 | 3 => "small",4..=9 => "medium",n if n < 0 => "negative",_ => "large",}}fn main() {for n in [-5, 0, 2, 7, 100] {println!("{} is {}", n, describe_number(n));}}
match on plain values: ranges, multiple values per arm, a guard condition, and a final wildcard arm that makes the match exhaustive.
fn main() {let config_max: Option<u8> = Some(3);// if let: handle just the case you care about, skip the boilerplate match.if let Some(max) = config_max {println!("Maximum is configured to be {}", max);} else {println!("No maximum configured");}// while let: keep matching and looping until the pattern no longer matches.let mut stack = vec![1, 2, 3, 4, 5];while let Some(top) = stack.pop() {println!("popped {}", top);}}
if let as a shortcut for a single interesting case, and while let to drain a Vec by repeatedly matching on pop()'s Option until it returns None.
🧠 Check your understanding
0/1 · 0/1 answered1. You write a match over an enum with 3 variants, handle only 2 of them explicitly, and add no `_` catch-all arm. What happens?