Skip to content
If Expressions and match
step 1/5

Reading — step 1 of 5

Learn

~3 min readControl Flow

If Expressions and match

Rust control flow has two headline differences from everything you've used: if is an expression (it produces a value), and match is a pattern-matching machine whose superpower is that the compiler proves you handled every case. Both change how you structure code, not just how you spell it.

if: no parens, always braces, produces values

if score >= 90 {
    println!("A");
} else if score >= 80 {
    println!("B");
} else {
    println!("F");
}

Familiar shape, two rules: no parentheses around the condition, braces mandatory. And the condition must be a real boolif 1 { doesn't compile; no truthiness, no memorizing falsy tables.

The expression part is where Rust departs:

let grade = if score >= 90 { "A" } else { "F" };

No ternary operator needed — if/else is the ternary, readable at any size. Note the missing semicolons inside the branches: a block's last expression, unterminated, is the block's value. (Both branches must produce the same type; the compiler checks.)

match: switch that finishes the job

let description = match day {
    "Sat" | "Sun" => "weekend",           // multiple patterns with |
    "Fri" => "almost",
    _ => "weekday",                        // _ = catch-all
};

Three properties make match the most-used construct in real Rust:

  1. It's an expression — arms produce values, as above.
  2. No fallthrough, no break — one arm runs, cleanly.
  3. Exhaustiveness: the compiler requires every possible value be covered. Match on a bool without a false arm, or (in chapter 5) on an enum missing a variant → compile error naming exactly what you forgot. Add a variant to an enum next month and every match in the codebase that needs updating fails to build until you update it. That's refactoring with a safety net no other mainstream language provides.

Patterns go well beyond literals:

match n {
    0 => println!("zero"),
    1..=9 => println!("single digit"),     // inclusive range pattern
    x if x < 0 => println!("negative"),    // guard clause
    _ => println!("big"),
}

Your exercise: FizzBuzz for one number

Multiples of 3 → Fizz, of 5 → Buzz, both → FizzBuzz, else the number. The classic ordering trap applies — check the "both" case first (first true branch wins; test 3 before 15 and the 15 case is unreachable). Two idiomatic shapes; pick either:

// if/else, specific-first
if n % 15 == 0 { println!("FizzBuzz") }
else if n % 3 == 0 { println!("Fizz") }
else if n % 5 == 0 { println!("Buzz") }
else { println!("{n}") }

// or the very-Rust tuple match — order still decides
match (n % 3 == 0, n % 5 == 0) {
    (true, true)  => println!("FizzBuzz"),
    (true, false) => println!("Fizz"),
    (false, true) => println!("Buzz"),
    (false, false) => println!("{n}"),
}

The tuple match is worth writing once even if you keep the if/else: all four combinations, visibly enumerated, compiler-verified complete. That's the match mindset — make the cases a checklist the compiler audits.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…