Skip to Content
Control Flow

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.

src/main.rs
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:

RuleWhat this means
The condition must be a boolif 1 { } is a compile error — Rust does not treat numbers as truthy
Curly braces are requiredif x > 0 println!("yes") does not compile
if is an expression, not a statementIt evaluates to a value and can be used on the right side of let
src/main.rs
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 integer

Rust 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:

src/main.rs
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"); } }
Hot

Rust 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:

OperatorNameMeaningExample
&&ANDBoth sides must be trueage >= 18 && has_id
||ORAt least one side must be trueis_admin || is_owner
!NOTInverts true to false and vice versa!is_banned

&& — AND (Both Must Be True)

src/main.rs
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)

src/main.rs
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)

src/main.rs
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:

src/main.rs
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!
ConditionValueReason
age >= 18true22 ≥ 18
has_tickettrueset above
age >= 18 && has_tickettrueboth true
is_bannedfalseset above
!is_bannedtrueNOT false = true
Full conditiontrueall 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:

src/main.rs
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 applied

650 also satisfies >= 200, but that branch never runs — Rust stops at the first condition that is true and skips everything below it.

Branch checkedcart_totalConditionRuns?
>= 1000650falseNo
>= 500650trueYes — stops here
>= 200skippedNo
elseskippedNo

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.

src/main.rs
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:

src/main.rs
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`
ArmType producedOutcome
42i32
"hello"&strCompile error — type mismatch
43i32OK — 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.

src/main.rs
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:

src/main.rs
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: 40

The 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:

src/main.rs
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.

src/main.rs
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

SituationPrefer
Exit condition is at the top, always a boolwhile
Exit can happen anywhere in the bodyloop
You need to return a value when breakingloop
Infinite server / event looploop

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.

src/main.rs
fn main() { let planets = ["Mercury", "Venus", "Earth", "Mars"]; for planet in planets { println!("{planet}"); } }
Mercury Venus Earth Mars

Because 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:

src/main.rs
fn main() { // exclusive range: 1, 2, 3, 4 for i in 1..5 { println!("{i}"); } }
1 2 3 4
src/main.rs
fn main() { // inclusive range: 1, 2, 3, 4, 5 for i in 1..=5 { println!("{i}"); } }
1 2 3 4 5

Reversing a Range

Chain .rev() to iterate in reverse order:

src/main.rs
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():

src/main.rs
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: TypeScript

continue — 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:

src/main.rs
fn main() { for n in 1..=10 { if n % 2 == 0 { continue; // skip even numbers } println!("{n}"); } }
1 3 5 7 9

break vs continue at a Glance

KeywordEffect
breakExits the current loop immediately
break valueExits and returns value from the loop expression
break 'labelExits the named outer loop
continueSkips the rest of this iteration and goes to the next
continue 'labelSkips 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:

src/main.rs
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 7

Summary

ConstructPurposeStops when
if / elseRun a block based on a condition— (runs once)
else ifChain multiple conditions— (runs once)
if in letAssign a value based on a condition— (runs once)
loopRepeat foreverbreak is hit
while condRepeat while condition holdsCondition becomes false
for x in iterStep through a collection or rangeCollection is exhausted
breakExit the innermost loop
break valueExit loop and return a value
continueSkip 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.

Last updated on