Skip to content

Control Flow and Pattern Matching

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.

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" };
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.

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 });

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 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
}
}
}

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.

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.

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 here

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 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"),
}

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 loops iterate over any type implementing IntoIterator. This includes arrays, vectors, Ranges, strings, hash maps, and any custom iterator:

// Array
let arr = [10, 20, 30, 40, 50];
for element in arr {
println!("the value is: {}", element);
}
// Range
for number in 1..=5 {
println!("{}", number);
}
// Reverse range
for number in (1..4).rev() {
println!("{}!", number);
}
// With index
for (index, value) in arr.iter().enumerate() {
println!("{}: {}", index, value);
}
// String characters
for ch in "hello".chars() {
println!("{}", ch);
}

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 immutably
for item in &v {
println!("{}", item);
}
// Borrow mutably
for item in &mut v {
*item += 1;
}
// Move out (v is consumed)
for item in v {
println!("{}", item);
}
// v is no longer usable here
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.

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);

Skips the rest of the current iteration and moves to the next:

for number in 1..=10 {
if number % 2 == 0 {
continue;
}
println!("{}", number);
}

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.

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 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 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 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, 1

while let is less common than for but useful when consuming values from an iterator with a Specific pattern.

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);
}
_ => {}
}

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"),
_ => {}
}

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 accessible
let Person { name, age } = person;
println!("{} is {}", name, age);
let person = Person {
name: String::from("Bob"),
age: 25,
};
// Borrow — person remains fully accessible
let Person { ref name, ref age } = person;
println!("{} is {}", name, age);
println!("{} is {}", person.name, person.age);
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"),
}
let arr = [1, 2, 3, 4, 5];
match arr {
[first, second, ..] => {
println!("first: {}, second: {}", first, second);
}
[] => {
println!("empty array");
}
}
// More specific slice matching
let 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"),
}

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);

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]);

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);
}

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),
}
}
}

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 contexts
let numbers = vec![Some(1), None, Some(3), None, Some(5)];
let has_some = numbers.iter().any(|x| matches!(x, Some(_)));
assert!(has_some);

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 &lt; 10 => println!("low connection limit: {}", n),
n if n &lt; 100 => println!("moderate connection limit: {}", n),
n => println!("high connection limit: {}", n),
}
## Cross-References
  • 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.