Control Flow
A short-order cook stands behind the rail with a stack of tickets in front of her. Ticket one says 1. She looks at the number, decides what plate to send out, slides the ticket aside, picks up ticket two. The whole shift is two things repeating — a decision per ticket, and a way to keep grabbing the next one. Control flow is the same idea inside a Rust program. The decision is if. The grab-the-next-one is loop, while, or for. Change which one you use and the kitchen runs the same orders four different ways.

The drill we will use to see all four is FizzBuzz. A British programmer named Imran Ghory wrote a blog post in 2007 complaining that grown adults applying for coding jobs could not write a program that printed the numbers from 1 to 100 — except every multiple of 3 printed Fizz, every multiple of 5 printed Buzz, and every multiple of 15 printed FizzBuzz. Jeff Atwood reposted the rant and within a week every interviewer in the industry was handing it to candidates at the whiteboard. The reason it stuck is that you cannot fake it. You need a way to look at one number and decide which of four things to print, and you need a way to walk through 100 of them. Decision plus repetition. Control flow.
The decision part is the easier piece. Rust spells it if, and the chain of else if branches reads top to bottom — the first one that matches wins, the rest are skipped. Notice there are no parentheses around the condition and the body always sits inside braces, even if it is one line. Rust took both rules from a language called ML in the 1970s, where they decided early on that the parentheses around if conditions were noise.
fn label(n: u32) -> String {
if n % 15 == 0 {
String::from("FizzBuzz")
} else if n % 3 == 0 {
String::from("Fizz")
} else if n % 5 == 0 {
String::from("Buzz")
} else {
n.to_string()
}
}The function returns a String because four different branches need to produce four different answers and they all have to be the same type at the end. The check for 15 has to come first — if you checked 3 before 15, every multiple of 15 would print Fizz and you would never reach the FizzBuzz branch. The first match wins. Read it top to bottom and the order of the rules is the order of the priorities.
Now the repetition. Rust gives you three keywords for "do this again" and they exist because three different bottlenecks forced them into being. The oldest is for, which John Backus put into FORTRAN in 1957 because scientists wanted to say "run this equation for i from 1 to 1000" without writing a thousand lines. The next is while, which a Dutch professor named Edsger Dijkstra pushed into ALGOL 60 because he wanted a loop whose stop condition was a question — keep going while this is true — rather than a count. The third is the bare loop, which Rust added later because sometimes you just want to spin forever and decide inside the body when to quit.

Start with the bare loop. It is the most honest of the three — it says "keep going" and leaves it to you to call break when you have had enough. The // allow:loop comment on the line is a note to the lint that says yes, I know what I am doing, there is a break inside.
fn cook_with_loop() {
println!("-- loop --");
let mut n: u32 = 1;
loop { // allow:loop has explicit break when n > 15
if n > 15 {
break;
}
println!("ticket {:>2} -> {}", n, label(n));
n += 1;
}
}The cook starts at ticket 1, checks if she is past 15, prints, and bumps the counter. The instant n > 15 is true she breaks out. The control structure does no thinking on its own — every part of the stop is wired by hand. That is why people reach for while instead.
fn cook_with_while() {
println!("-- while --");
let mut n: u32 = 1;
while n <= 15 {
println!("ticket {:>2} -> {}", n, label(n));
n += 1;
}
}The shape is shorter because while already knows how to ask the question. Same output, same counter, one less moving part. But the counter is still yours to manage. You declare mut n, you remember to n += 1 at the bottom of the body, and if you forget that line the cook stands at ticket 1 forever. Dijkstra knew this. He spent the next twenty years writing papers arguing that even while was too easy to get wrong, which is part of why for exists in the shape Rust gives you.
fn cook_with_for() {
println!("-- for --");
for n in 1..=15 {
println!("ticket {:>2} -> {}", n, label(n));
}
}The 1..=15 is a range — a tiny object that knows how to hand you the numbers 1 through 15 in order, one at a time, and stop. The for keyword pulls them out one by one and binds each to n inside the loop. No counter to declare, no counter to bump, no stop condition to maintain. The cook just gets handed the tickets. Almost every loop you write in Rust will look like this, because almost every loop is "for each thing in some collection, do the work."
The fourth way is the same for written as a chain of methods, which is how a lot of real Rust code looks once you are comfortable with it.
fn cook_with_iter() {
println!("-- iterator chain --");
(1u32..=15)
.map(|n| format!("ticket {:>2} -> {}", n, label(n)))
.for_each(|line| println!("{}", line));
}The range pipes into .map, which transforms each number into the line we want to print, and then into .for_each, which sends each line to println!. No mutable variables anywhere. Reading the chain left to right tells you the whole pipeline — take the numbers, label them, print them. We will spend a whole lesson on these iterator chains soon, but for now the shape is enough.
The driver is small. Call each cook in order.
fn main() {
cook_with_loop();
cook_with_while();
cook_with_for();
cook_with_iter();
println!("all four cooks plated the same 15 tickets.");
}Run it and you see the same fifteen tickets plated four times, once by each cook. The headers between the sections let you watch which loop produced which run.
-- loop --
ticket 1 -> 1
ticket 2 -> 2
ticket 3 -> Fizz
ticket 4 -> 4
ticket 5 -> Buzz
ticket 6 -> Fizz
ticket 7 -> 7
ticket 8 -> 8
ticket 9 -> Fizz
ticket 10 -> Buzz
ticket 11 -> 11
ticket 12 -> Fizz
ticket 13 -> 13
ticket 14 -> 14
ticket 15 -> FizzBuzz
-- while --
ticket 1 -> 1
ticket 2 -> 2
ticket 3 -> Fizz
ticket 4 -> 4
ticket 5 -> Buzz
ticket 6 -> Fizz
ticket 7 -> 7
ticket 8 -> 8
ticket 9 -> Fizz
ticket 10 -> Buzz
ticket 11 -> 11
ticket 12 -> Fizz
ticket 13 -> 13
ticket 14 -> 14
ticket 15 -> FizzBuzz
-- for --
ticket 1 -> 1
ticket 2 -> 2
ticket 3 -> Fizz
ticket 4 -> 4
ticket 5 -> Buzz
ticket 6 -> Fizz
ticket 7 -> 7
ticket 8 -> 8
ticket 9 -> Fizz
ticket 10 -> Buzz
ticket 11 -> 11
ticket 12 -> Fizz
ticket 13 -> 13
ticket 14 -> 14
ticket 15 -> FizzBuzz
-- iterator chain --
ticket 1 -> 1
ticket 2 -> 2
ticket 3 -> Fizz
ticket 4 -> 4
ticket 5 -> Buzz
ticket 6 -> Fizz
ticket 7 -> 7
ticket 8 -> 8
ticket 9 -> Fizz
ticket 10 -> Buzz
ticket 11 -> 11
ticket 12 -> Fizz
ticket 13 -> 13
ticket 14 -> 14
ticket 15 -> FizzBuzz
all four cooks plated the same 15 tickets.Look at ticket 15 in any of the four sections. Why does it print FizzBuzz and not Fizz or Buzz? Because the label function checks n % 15 == 0 first, and 15 divides cleanly into 15, so that branch wins before the other two get a chance. Move that check to the bottom of the chain and ticket 15 prints Fizz instead. The decision tree's order is the program's behavior.
Next lesson — what happens when the decision is not "this number divides by this other number" but "this value is one of six possible shapes," and how Rust's match keyword lets the compiler refuse to compile if you forget to handle one of them.