Control Flow and Pattern Matching
Intuition
Section titled “Intuition”Control flow in Rust is expression-oriented, meaning nearly everything returns a value. Pattern matching with match is like a Swiss Army knife for branching, destructuring data while simultaneously making decisions. The if expression eliminates the need for ternary operators by treating branches as value-producing blocks. Enums with associated data enable algebraic data types where the compiler ensures exhaustive handling of all cases, preventing forgotten branches at compile time rather than runtime.
if / else
Section titled “if / else”Rust’s if expression does not require parentheses around the condition, but braces around the body Are mandatory. Unlike C or Java, if is an expression — it returns a value and can be used inline:
let condition = true;let number = if condition { 5 } else { 6 };assert_eq!(number, 5);Both branches must produce the same type. The compiler enforces this:
// ERROR: if and else have incompatible types// let x = if true { 5 } else { "six" };else if Chains
Section titled “else if Chains”let number = 6;
if number % 4 == 0 { println!("number is divisible by 4");} else if number % 3 == 0 { println!("number is divisible by 3");} else if number % 2 == 0 { println!("number is divisible by 2");} else { println!("number is not divisible by 4, 3, or 2");}There is no ternary operator (? :). The if expression is the ternary.
Conditional Assignment Patterns
Section titled “Conditional Assignment Patterns”Use if expressions for concise conditional initialization:
let port = std::env::var("PORT") .ok() .and_then(|s| s.parse::<u16>().ok()) .unwrap_or(if cfg!(debug_assertions) { 3000 } else { 8080 });Blocks as Expressions
Section titled “Blocks as Expressions”Every block in Rust is an expression. The last expression without a semicolon is the return value:
let x = { let a = 1; let b = 2; a + b // no semicolon — this is the block's value};assert_eq!(x, 3);
let y = { let a = 1; let b = 2; a + b; // semicolon — the block returns ()};assert_eq!(y, ());This is not specific to if — it applies to match arms, loop bodies, function bodies, and any Braced block.
match Expressions
Section titled “match Expressions”match is Rust’s most powerful control flow construct. It performs exhaustive pattern matching Against a value and executes the first matching arm.
enum Coin { Penny, Nickel, Dime, Quarter(String),}
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 } }}Exhaustiveness
Section titled “Exhaustiveness”The compiler verifies that every possible value is covered. Adding a new variant to an enum causes Every match on that enum to produce a compile error until updated:
enum Color { Red, Green, Blue }
fn color_name(c: Color) -> &'static str { match c { Color::Red => "red", Color::Green => "green", // Color::Blue => "blue", // if you comment this out, compiler errors _ => "other", // wildcard covers remaining variants }}The _ wildcard matches anything and is useful when you do not need to handle specific cases. It Does not bind the value.
Match Guards
Section titled “Match Guards”Add an if condition to a match arm for additional filtering:
let num = Some(4);
match num { Some(x) if x % 2 == 0 => println!("even: {}", x), Some(x) => println!("odd: {}", x), None => println!("none"),}Match guards do not participate in exhaustiveness checking. The compiler cannot prove that a guard Will always match, so you may still need a catch-all arm.
Variable Shadowing in Match Arms
Section titled “Variable Shadowing in Match Arms”Match arms introduce new scopes. Variables bound in one arm do not leak to others:
let x = 5;
match x { 1 => println!("one"), 2 => println!("two"), y => println!("something else: {}", y),}// y is not in scope hereMatch on References (Match Ergonomics)
Section titled “Match on References (Match Ergonomics)”Rust 2021 edition enables match ergonomics — the compiler automatically adds & when matching Through a reference:
let c = Coin::Penny;let r: &Coin = &c;
match r { Coin::Penny => println!("penny"), // auto-dereferenced Coin::Nickel => println!("nickel"), _ => {}}Match as a Destructuring Tool
Section titled “Match as a Destructuring Tool”match is the primary mechanism for destructuring tuples, structs, and enums:
struct Point { x: f64, y: f64 }let p = Point { x: 0.0, y: 7.0 };
match p { Point { x: 0.0, y } => println!("on y-axis at {}", y), Point { x, y: 0.0 } => println!("on x-axis at {}", x), Point { x, y } => println!("at ({}, {})", x, y),}
let pair = (2, -2);match pair { (0, y) => println!("x is zero, y is {}", y), (x, 0) => println!("x is {}, y is zero", x), _ => println!("neither is zero"),}Or Patterns (Rust 1.53+)
Section titled “Or Patterns (Rust 1.53+)”Match multiple patterns with |:
let x = 1;
match x { 1 | 2 => println!("one or two"), 3..=5 => println!("three through five"), _ => println!("something else"),}Guarding Against Variable Binding Conflicts
Section titled “Guarding Against Variable Binding Conflicts”A match guard can reference variables from the enclosing scope. If the guard’s variable name shadows The pattern’s binding, use @ to bind and filter simultaneously:
let age = 15;
match age { n @ 1..=12 => println!("child: {}", n), n @ 13..=19 => println!("teenager: {}", n), n => println!("adult: {}", n),}The loop keyword creates an infinite loop. Use break to exit:
let mut counter = 0;
let result = loop { counter += 1;
if counter == 10 { break counter * 2; // break can return a value }};
assert_eq!(result, 20);loop is the only loop that returns a value from break. while and for loops do not support Value-returning breaks (they return ()).
let mut number = 3;
while number != 0 { println!("{}!", number); number -= 1;}while evaluates the condition before each iteration. If the condition is false on the first check, The body never executes.
for — Iterator-Based Looping
Section titled “for — Iterator-Based Looping”for loops iterate over any type implementing IntoIterator. This includes arrays, vectors, Ranges, strings, hash maps, and any custom iterator:
// Arraylet arr = [10, 20, 30, 40, 50];for element in arr { println!("the value is: {}", element);}
// Rangefor number in 1..=5 { println!("{}", number);}
// Reverse rangefor number in (1..4).rev() { println!("{}!", number);}
// With indexfor (index, value) in arr.iter().enumerate() { println!("{}: {}", index, value);}
// String charactersfor ch in "hello".chars() { println!("{}", ch);}Iterating Over References
Section titled “Iterating Over References”for item in &collection borrows each element. for item in &mut collection borrows each element Mutably. for item in collection moves each element:
let mut v = vec![1, 2, 3, 4, 5];
// Borrow immutablyfor item in &v { println!("{}", item);}
// Borrow mutablyfor item in &mut v { *item += 1;}
// Move out (v is consumed)for item in v { println!("{}", item);}// v is no longer usable herefor Over Maps
Section titled “for Over Maps”use std::collections::HashMap;
let mut scores = HashMap::new();scores.insert(String::from("Blue"), 10);scores.insert(String::from("Red"), 50);
for (key, value) in &scores { println!("{}: {}", key, value);}HashMap iteration order is not guaranteed. If you need ordering, collect into a BTreeMap or sort The entries.
break and continue
Section titled “break and continue”Exits the current loop immediately. In a loop``break can return a value:
let mut i = 0;loop { i += 1; if i == 5 { break; }}assert_eq!(i, 5);continue
Section titled “continue”Skips the rest of the current iteration and moves to the next:
for number in 1..=10 { if number % 2 == 0 { continue; } println!("{}", number);}Labeled break and continue
Section titled “Labeled break and continue”Labels allow breaking out of or continuing an outer loop from within a nested loop:
'outer: for x in 0..5 { for y in 0..5 { if x == 2 && y == 2 { break 'outer; // breaks out of the outer loop } if y == 0 { continue 'outer; // continues the outer loop } println!("x={}, y={}", x, y); }}Labels use a single quote prefix ('outer:) and are referenced with break 'label or continue 'label.
Labeled Loops Returning Values
Section titled “Labeled Loops Returning Values”A labeled loop can return a value via break:
let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
let found = 'search: loop { for row in &matrix { for &val in row { if val == 5 { break 'search val; // returns 5 from the outer loop } } } panic!("not found");};
assert_eq!(found, 5);if let and while let
Section titled “if let and while let”if let
Section titled “if let”if let is syntactic sugar for a match with a single arm of interest:
let some_value = Some(7);
if let Some(n) = some_value { println!("value is {}", n);}This is equivalent to:
match some_value { Some(n) => println!("value is {}", n), _ => {},}if let does not enforce exhaustiveness. Use it when you care about one pattern and want to ignore The rest. The else block handles the non-matching case:
let some_value = Some(7);
if let Some(n) = some_value { println!("value is {}", n);} else { println!("no value");}let-else (Rust 1.65+)
Section titled “let-else (Rust 1.65+)”let-else combines pattern matching with early return. The else block must diverge:
fn process(data: Option<Vec<i32>>) -> i32 { let Some(values) = data else { return 0; }; values.iter().sum()}
fn parse_or_default(s: &str) -> u64 { let Ok(n) = s.parse::<u64>() else { return 0; }; n}The else block can contain return``break``continue``panic!Or another diverging Expression. It cannot be a non-diverging block.
while let
Section titled “while let”while let repeatedly matches a pattern and runs the loop body until the pattern stops matching:
let mut stack = Vec::new();stack.push(1);stack.push(2);stack.push(3);
while let Some(top) = stack.pop() { println!("{}", top);}// Prints: 3, 2, 1while let is less common than for but useful when consuming values from an iterator with a Specific pattern.
match Ergonomics in Depth
Section titled “match Ergonomics in Depth”Destructuring Nested Structures
Section titled “Destructuring Nested Structures”enum Message { Hello { id: usize }, Move { x: i32, y: i32 }, Write(String), ChangeColor(u8, u8, u8),}
let msg = Message::ChangeColor(255, 0, 128);
match msg { Message::ChangeColor(r, g, b) if r == 255 => { println!("pure red component: r={}, g={}, b={}", r, g, b); } Message::ChangeColor(r, g, b) => { println!("color: rgb({}, {}, {})", r, g, b); } Message::Write(text) => { println!("message: {}", text); } _ => {}}Ignoring Values with ..
Section titled “Ignoring Values with ..”Use .. to ignore remaining fields in a struct or tuple:
struct Point3D { x: f64, y: f64, z: f64 }let p = Point3D { x: 1.0, y: 2.0, z: 3.0 };
match p { Point3D { x, .. } => println!("x is {}", x),}
let origin = (0, 0, 0);match origin { (0, ..) => println!("x is zero"), _ => {}}Binding with ref and ref mut
Section titled “Binding with ref and ref mut”When destructuring, values are moved by default. Use ref to borrow and ref mut to borrow Mutably:
struct Person { name: String, age: u32 }let person = Person { name: String::from("Alice"), age: 30,};
// Move — person.name is no longer accessiblelet Person { name, age } = person;println!("{} is {}", name, age);
let person = Person { name: String::from("Bob"), age: 25,};
// Borrow — person remains fully accessiblelet Person { ref name, ref age } = person;println!("{} is {}", name, age);println!("{} is {}", person.name, person.age);Matching Literals and Constants
Section titled “Matching Literals and Constants”const MAX_POINTS: u32 = 100_000;
let points = 75_000;
match points { 0 => println!("zero points"), MAX_POINTS => println!("max points"), 1..=10_000 => println!("low points"), _ => println!("some points"),}Matching Slices and Arrays
Section titled “Matching Slices and Arrays”let arr = [1, 2, 3, 4, 5];
match arr { [first, second, ..] => { println!("first: {}, second: {}", first, second); } [] => { println!("empty array"); }}
// More specific slice matchinglet slice = &[1, 2, 3, 4, 5];match slice { [1, 2, rest @ ..] => println!("starts with 1,2, rest: {:?}", rest), [1, .., 5] => println!("starts with 1, ends with 5"), _ => println!("other"),}Loop Expressions and Patterns
Section titled “Loop Expressions and Patterns”Loop as an Expression
Section titled “Loop as an Expression”Since loop is an expression, it can be used in value context:
let mut count = 0;let result = loop { count += 1; if count == 10 { break count; }};assert_eq!(result, 10);Early Exit from Nested Loops with Labels
Section titled “Early Exit from Nested Loops with Labels”Labels enable complex control flow in nested loops:
let mut matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
'outer: for row in &mut matrix { for cell in row.iter_mut() { *cell *= 2; if *cell == 10 { break 'outer; } }}assert_eq!(matrix[0], [2, 4, 6]);assert_eq!(matrix[1], [8, 10, 12]);assert_eq!(matrix[2], [7, 8, 9]);while with Iterator
Section titled “while with Iterator”You can use while let with an iterator, though for is generally preferred:
let mut iter = vec![1, 2, 3].into_iter();
while let Some(value) = iter.next() { println!("{}", value);}loop with ? Operator
Section titled “loop with ? Operator”Use loop with ? for retry patterns:
fn fetch_with_retry(url: &str, max_retries: usize) -> Result<String, reqwest::Error> { let mut retries = 0; loop { match reqwest::get(url) { Ok(response) => return Ok(response.text()?), Err(e) if retries < max_retries => { retries += 1; std::thread::sleep(std::time::Duration::from_secs(1)); continue; } Err(e) => return Err(e), } }}The matches! Macro
Section titled “The matches! Macro”matches! is a concise way to check whether a value matches a pattern — it returns bool:
let x = Some(5);
assert!(matches!(x, Some(5)));assert!(matches!(x, Some(_)));assert!(!matches!(x, None));assert!(matches!(x, Some(n) if n > 3));
// Useful in filter contextslet numbers = vec![Some(1), None, Some(3), None, Some(5)];let has_some = numbers.iter().any(|x| matches!(x, Some(_)));assert!(has_some);Guard Expressions in Match Arms
Section titled “Guard Expressions in Match Arms”Match guards enable conditional logic that depends on values outside the pattern:
struct Config { max_connections: u32, connection_timeout_ms: u64,}
let config = Config { max_connections: 100, connection_timeout_ms: 5000,};
match config.max_connections { n if n == 0 => println!("connections disabled"), n if n < 10 => println!("low connection limit: {}", n), n if n < 100 => println!("moderate connection limit: {}", n), n => println!("high connection limit: {}", n),}- Types and Variables: Provides the foundation of Rust’s type system, which is essential for understanding pattern matching and control flow expressions.
- Ownership and Borrowing: Explains Rust’s memory safety guarantees that enable fearless control flow without runtime checks.
- Structs and Enums: Shows how to define the data types that pattern matching operates on, including enums with associated data.
- Error Handling: Builds on pattern matching with Result and Option types for robust error handling in control flow.