Control Flow in Rust
Control flow is how you tell a program which code to run and how many times to run it. Without it, every program would execute every line from top to bottom, once, no matter what. That is rarely useful.
Rust gives you two main tools for control flow:
- Conditionals — run a block of code only when a condition is true
- Loops — repeat a block of code until something changes
How a Condition Works
if Expressions
An if expression evaluates a condition and runs one of two branches depending on whether that condition is true or false.
fn main() {
let score = 72;
if score >= 60 {
println!("You passed!");
} else {
println!("You did not pass.");
}
}You passed!Rules Rust Enforces
Unlike C, JavaScript, or Python, Rust is strict about what counts as a condition:
| Rule | What this means |
|---|---|
The condition must be a bool | if 1 { } is a compile error — Rust does not treat numbers as truthy |
| Curly braces are required | if x > 0 println!("yes") does not compile |
if is an expression, not a statement | It evaluates to a value and can be used on the right side of let |
fn main() {
let ready = 1; // a number, not a bool
if ready { // ✗ compile error
println!("go!");
}
}error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if ready {
| ^^^^^ expected `bool`, found integerRust forces you to be explicit. If you mean “is this non-zero?”, write if ready != 0. This eliminates an entire class of bugs where a number is accidentally used where a boolean was intended.
else if — Multiple Branches
When you have more than two cases, chain conditions with else if:
fn main() {
let temperature = 35;
if temperature < 0 {
println!("Freezing");
} else if temperature < 15 {
println!("Cold");
} else if temperature < 25 {
println!("Comfortable");
} else {
println!("Hot");
}
}HotRust checks each condition in order and runs only the first branch that matches. The rest are skipped.
If you have many else if branches, consider using a match expression instead — it is more readable and the compiler enforces that you handle every case.
Handling Multiple Conditions
Sometimes a single condition is not enough — you need to check two or more things at once. Rust gives you three logical operators for this:
| Operator | Name | Meaning | Example |
|---|---|---|---|
&& | AND | Both sides must be true | age >= 18 && has_id |
|| | OR | At least one side must be true | is_admin || is_owner |
! | NOT | Inverts true to false and vice versa | !is_banned |
&& — AND (Both Must Be True)
fn main() {
let age = 20;
let has_ticket = true;
if age >= 18 && has_ticket {
println!("Entry granted.");
} else {
println!("Entry denied.");
}
}Entry granted.Both conditions must hold. If age were 16, or has_ticket were false, the whole condition would be false.
|| — OR (At Least One Must Be True)
fn main() {
let is_admin = false;
let is_owner = true;
if is_admin || is_owner {
println!("Access allowed.");
} else {
println!("Access denied.");
}
}Access allowed.Only one side needs to be true. Rust also short-circuit evaluates — if the left side of || is already true, the right side is never evaluated.
! — NOT (Invert a Condition)
fn main() {
let is_banned = false;
if !is_banned {
println!("Welcome back!");
} else {
println!("You are banned.");
}
}Welcome back!!is_banned reads as “if the user is not banned”. It flips false to true and true to false.
Combining All Three
You can chain multiple operators in one condition. Use parentheses to make the grouping explicit and avoid confusion:
fn main() {
let age = 22;
let has_ticket = true;
let is_banned = false;
if (age >= 18 && has_ticket) && !is_banned {
println!("Come on in!");
} else {
println!("Cannot enter.");
}
}Come on in!| Condition | Value | Reason |
|---|---|---|
age >= 18 | true | 22 ≥ 18 |
has_ticket | true | set above |
age >= 18 && has_ticket | true | both true |
is_banned | false | set above |
!is_banned | true | NOT false = true |
| Full condition | true | all parts satisfied |
Add parentheses whenever you mix && and || in the same condition. Without them, && binds more tightly than || — which can produce surprising results if you are not careful.
Multiple else if Branches on One Value
A common pattern is testing the same variable against several thresholds in sequence. Here a cart total qualifies for different discount tiers — only the highest matching tier applies:
fn main() {
let cart_total = 650;
if cart_total >= 1000 {
println!("20% discount applied");
} else if cart_total >= 500 {
println!("15% discount applied");
} else if cart_total >= 200 {
println!("10% discount applied");
} else {
println!("No discount");
}
}15% discount applied650 also satisfies >= 200, but that branch never runs — Rust stops at the first condition that is true and skips everything below it.
| Branch checked | cart_total | Condition | Runs? |
|---|---|---|---|
>= 1000 | 650 | false | No |
>= 500 | 650 | true | Yes — stops here |
>= 200 | — | skipped | No |
else | — | skipped | No |
This is why order matters — place the most restrictive condition first. If you put >= 200 at the top, every cart above 200 would always get the 10% tier and the higher discounts would never trigger.
if in a let Statement
Because if is an expression, its result can be assigned directly to a variable. This is one of Rust’s most elegant features — it replaces the ternary operator (? :) found in other languages.
fn main() {
let is_weekend = true;
let greeting = if is_weekend {
"Enjoy your day off!"
} else {
"Have a productive workday!"
};
println!("{}", greeting);
}Enjoy your day off!Both Arms Must Return the Same Type
Because the variable greeting can only have one type, both branches of the if must produce the same type:
fn main() {
let condition = true;
let value = if condition {
42 // i32
} else {
"hello" // &str — type mismatch!
};
}error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
4 | 42
| -- expected because of this
5 | "hello"
| ^^^^^^^ expected integer, found `&str`| Arm | Type produced | Outcome |
|---|---|---|
42 | i32 | — |
"hello" | &str | Compile error — type mismatch |
43 | i32 | OK — both arms agree |
Loops
Rust has three loop constructs. Each serves a different purpose:
loop — Repeat Forever
loop runs a block of code endlessly until you explicitly stop it with break.
fn main() {
let mut count = 0;
loop {
count += 1;
println!("count = {count}");
if count == 3 {
break;
}
}
println!("Done.");
}count = 1
count = 2
count = 3
Done.Returning a Value from loop
You can pass a value out of a loop block by placing it after break. This is useful for retrying an operation until it succeeds:
fn main() {
let mut attempts = 0;
let result = loop {
attempts += 1;
if attempts == 4 {
break attempts * 10; // returns 40
}
};
println!("Succeeded after {} attempts. Result: {}", attempts, result);
}Succeeded after 4 attempts. Result: 40The value after break becomes the value of the entire loop expression — similar to how the last expression in a block becomes the block’s value.
Loop Labels — Controlling Nested Loops
When you have a loop inside another loop, break and continue apply to the innermost loop by default. To target an outer loop, use a loop label:
fn main() {
'outer: for row in 0..3 {
for col in 0..3 {
if row == 1 && col == 1 {
println!("Breaking out of both loops at row={row}, col={col}");
break 'outer; // exits the outer for loop entirely
}
println!("row={row}, col={col}");
}
}
println!("Finished.");
}row=0, col=0
row=0, col=1
row=0, col=2
row=1, col=0
Breaking out of both loops at row=1, col=1
Finished.while — Condition-Based Loop
A while loop checks a condition before each iteration. When the condition becomes false, the loop stops.
fn main() {
let mut fuel = 5;
while fuel > 0 {
println!("Fuel remaining: {fuel}");
fuel -= 1;
}
println!("Tank empty.");
}Fuel remaining: 5
Fuel remaining: 4
Fuel remaining: 3
Fuel remaining: 2
Fuel remaining: 1
Tank empty.while is essentially loop + if condition { break } combined into one readable construct. Use while when you have a clear boolean condition to check. Use loop when the exit condition is more complex or happens mid-body.
loop vs while — When to Use Which
| Situation | Prefer |
|---|---|
Exit condition is at the top, always a bool | while |
| Exit can happen anywhere in the body | loop |
| You need to return a value when breaking | loop |
| Infinite server / event loop | loop |
for — Iterating Over a Collection
for is Rust’s most commonly used loop. It iterates over every element in a collection or range without needing a manual index or a bound check.
fn main() {
let planets = ["Mercury", "Venus", "Earth", "Mars"];
for planet in planets {
println!("{planet}");
}
}Mercury
Venus
Earth
MarsBecause for automatically stops when the collection runs out, there is no risk of going out of bounds — unlike manually incrementing an index in a while loop.
Iterating Over a Range
Rust has built-in range syntax for generating sequences of numbers:
fn main() {
// exclusive range: 1, 2, 3, 4
for i in 1..5 {
println!("{i}");
}
}1
2
3
4fn main() {
// inclusive range: 1, 2, 3, 4, 5
for i in 1..=5 {
println!("{i}");
}
}1
2
3
4
5Reversing a Range
Chain .rev() to iterate in reverse order:
fn main() {
for i in (1..=5).rev() {
println!("{i}");
}
println!("Liftoff!");
}5
4
3
2
1
Liftoff!Accessing the Index with .enumerate()
When you need both the index and the value, use .enumerate():
fn main() {
let languages = ["Rust", "Go", "Python", "TypeScript"];
for (index, lang) in languages.iter().enumerate() {
println!("{}: {}", index + 1, lang);
}
}1: Rust
2: Go
3: Python
4: TypeScriptcontinue — Skip the Rest of This Iteration
continue jumps immediately to the next iteration of the loop, skipping whatever remains in the loop body for the current pass:
fn main() {
for n in 1..=10 {
if n % 2 == 0 {
continue; // skip even numbers
}
println!("{n}");
}
}1
3
5
7
9break vs continue at a Glance
| Keyword | Effect |
|---|---|
break | Exits the current loop immediately |
break value | Exits and returns value from the loop expression |
break 'label | Exits the named outer loop |
continue | Skips the rest of this iteration and goes to the next |
continue 'label | Skips to the next iteration of the named outer loop |
Putting It All Together
Here is a small program that combines if, for, and break to find the first number in a list divisible by a given divisor:
fn main() {
let numbers = [13, 7, 42, 19, 100, 3];
let divisor = 7;
let mut found = false;
for &n in numbers.iter() {
if n % divisor == 0 {
println!("{n} is divisible by {divisor}");
found = true;
break;
}
}
if !found {
println!("No number is divisible by {divisor}");
}
}7 is divisible by 7Summary
| Construct | Purpose | Stops when |
|---|---|---|
if / else | Run a block based on a condition | — (runs once) |
else if | Chain multiple conditions | — (runs once) |
if in let | Assign a value based on a condition | — (runs once) |
loop | Repeat forever | break is hit |
while cond | Repeat while condition holds | Condition becomes false |
for x in iter | Step through a collection or range | Collection is exhausted |
break | Exit the innermost loop | — |
break value | Exit loop and return a value | — |
continue | Skip to next iteration | — |
Loop labels 'name: | Target an outer loop with break/continue | — |
What’s Next?
You now have all the tools to direct the flow of any Rust program. The next step is Ownership — Rust’s most distinctive feature, which governs how memory is managed without a garbage collector.