Rust Functions and Control Flow: if, loop, while, for Explained

Rust Functions and Control Flow: Expressions vs. Statements
If you have programmed in C, Java, or JavaScript, the syntax of Rust's control flow and functions will look very familiar. However, beneath the curly braces, Rust operates on a fundamentally different paradigm.
Rust is overwhelmingly an Expression-based language, whereas languages like C are Statement-based. Understanding this distinction is the key to writing idiomatic, clean, and concise Rust code.
In this module, we will explore the mechanics of Rust functions, the radical power of expressions, and the robust toolset Rust provides for conditional branching and looping.
1. Anatomy of a Rust Function
Functions are prevalent in Rust code. You've already seen one of the most important functions in the language: main, which is the entry point of many programs. You define new functions using the fn keyword.
fn main() {
println!("Init execution.");
execute_transaction(10052, 450);
}
// Function with typed parameters
fn execute_transaction(transaction_id: u32, amount: i32) {
println!("Processing TX: {} for amount: ${}", transaction_id, amount);
}Signatures are Contracts
In Rust, you must declare the type of each parameter natively in the function signature. This is a deliberate design decision. By requiring explicit types in signatures, the compiler's Type Inference engine does not have to guess what your inputs might be, drastically reducing compile times and creating absolute clarity at the API boundary.
Return Values
To return a value from a function, we declare the type using an arrow ->.
fn calculate_hash(seed: i32) -> i32 {
let multiplier = 5;
seed * multiplier
}Wait, where is the return keyword? Where is the semicolon?
This brings us to the most important philosophical concept in this module.
2. Statements vs. Expressions
Rust code is a matrix of Statements and Expressions.
- Statements: Instructions that perform an action but do not return a value.
- Expressions: Evaluate to a resulting value.
Because statements do not return a value, you cannot chain them or assign them.
fn main() {
// This is a statement. `let` binds the value, it returns nothing.
let y = 6;
// ERROR! let y = 6 does not return a value, so x cannot be assigned.
// let x = (let y = 6);
let x = {
let a = 3;
// This is an expression. Note the lack of a semicolon!
a + 1
};
// x is now 4
}In the calculate_hash function earlier, seed * multiplier lacked a semicolon. Therefore, the block evaluated to that value, and the function naturally returned it. If you add a semicolon (seed * multiplier;), the expression becomes a statement that evaluates to (), an empty unit type, resulting in a compile-time error!
This paradigm allows you to write incredibly concise, fluid code without cluttering your syntax with explicit return instructions unless you are evaluating an early exit condition.
3. Conditional Flow: if Expressions
Because if blocks are expressions in Rust (they can evaluate to values), you can use them directly on the right side of a let statement.
fn main() {
let condition = true;
// This eliminates the need for ternary operators (?:)
let state = if condition {
"ACTIVE"
} else {
"FAILED"
};
println!("The system is: {}", state);
}[!WARNING] Because
stateexpects a single, fixed type to be allocated in memory, the return types of all branches in theifexpression must match. Returning"ACTIVE"(a string) in one branch and404(an integer) in theelsebranch will immediately trigger a compiler Type Matching error.
4. The Iteration Matrix: Loops
Rust has three kinds of loops: loop, while, and for.
The Infinite loop
The loop keyword tells Rust to execute a block of code forever, or until you explicitly tell it to halt using a break keyword. Because loop is also an expression, you can return values from it!
This is highly useful for thread-polling, retry systems, or waiting for a specific IO thread to finish execution.
fn main() {
let mut retry_count = 0;
let result = loop {
retry_count += 1;
if retry_count == 3 {
// Break out of the loop AND return the value!
break retry_count * 10;
}
};
println!("The result is {}", result); // 30
}The Conditional while
The while loop runs code so long as a specific condition evaluates to true. It is syntactic sugar roughly equivalent to a loop wrapping an if / break combination.
fn main() {
let mut countdown = 3;
while countdown != 0 {
println!("Launch in {}!", countdown);
countdown -= 1;
}
println!("LIFTOFF!!!");
}The Iterator for
The for loop is by far the most commonly used looping construct in Rust. It is safe, concise, and relies heavily on Rust's Iterator trait.
If you attempt to loop through an array using a while loop and an index parameter (e.g., let index = 0; index < 5; index++), you risk creating a runtime panic if the array changes size. A for loop guarantees compiler safety because it interacts entirely with the scope of the collection itself.
fn main() {
let servers = ["US-East", "US-West", "EU-Central"];
// Safely iterate over the array without an index bounds risk
for node in servers {
println!("Pinging node: {}", node);
}
// Looping over a Range
for number in (1..4).rev() { // 1 to 3 (exclusive 4), reversed!
println!("{}!", number);
}
}Summary and Next Steps
Rust is an expression-oriented language. The realization that almost everything—from a simple 5 + 5 to a massive if / else block to an infinite loop—can intrinsically evaluate to, and return, a value, completely changes how you architect your code structure. It drastically reduces reliance on mutable state and complex boilerplate.
You now possess the foundational syntax necessary to write standard applications. However, we haven't touched the memory management layer that makes Rust famous.
In the next module, we confront the core architectural paradigm of the entire language. We are going to explore why C crashes, why Java stutters, and how Ownership solves both problems mathematically.
This is the gateway to the Rust language.
Read next: Rust Ownership: The Core Concept Every Rust Developer Must Master →
Quick Knowledge Check
In Rust, what happens if you place a semicolon at the end of the last line of a function that specifies an integer return type?
- The code compiles successfully; the semicolon is optional syntactic sugar.
- The code fails to compile because placing a semicolon turns the final expression into a statement, returning an empty tuple () instead of the expected integer. ✓
- The compiler issues a warning but injects an implicit return 0 fallback.
- The code executes infinitely because the execution thread cannot locate a return exit block.
Explanation: In Rust, an expression lacking a semicolon evaluates to the return value of its block. Appending a semicolon transforms it into a statement, terminating its evaluation and causing it to return (), resulting in a Type Mismatch compiler error if the signature specified a return type like i32.
