HOW-TO: Functions and Control Flow in Rust
Master Rust functions with parameters, return types, and control flow. Learn if/else, loops (for, while, loop), match expressions, and when to use each. Includes common mistakes and worked examples from temperature conversion to FizzBuzz.
HOW-TO: Functions and Control Flow in Rust
Overview
Functions are the building blocks of every program. Control flow lets you make decisions and repeat actions. Together, they let you write meaningful code.
This guide walks you through:
- Writing functions with parameters and return types
- The Rust philosophy: statements vs. expressions (the semicolon trap!)
- Making decisions: if/else and match
- Repeating actions: for loops, while loops, and infinite loops
- When to use each and why
Prerequisites: You should be comfortable with Howto Rust Getting Started. Understanding how to compile and run Rust is essential.
Before You Start
Verify Your Setup
# Create a workspace for this article's examples
mkdir -p ~/rust-functions
cd ~/rust-functions
All code examples compile with:
rustc example.rs
./example
Functions: The Basics
Declaring a Function
fn main() {
greet();
}
fn greet() {
println!("Hello, Rust!");
}
Key points:
fnkeyword declares a function- Function names use
snake_case(lowercase, underscores for multiple words) - Parentheses
()are required, even with no parameters - Opening brace
{stays on the same line (Rust style) - Function body goes inside
{ }
Naming convention:
fn calculate_sum() { } // ✅ Good: verb + noun
fn user_input() { } // ✅ Good: describes action
fn greet() { } // ✅ Good: clear purpose
fn CalcSum() { } // ❌ Wrong: PascalCase (for types only)
fn calc_tot() { } // ❌ Wrong: abbreviations
fn cs() { } // ❌ Wrong: unclear
Function Order
Functions can be defined in any order:
fn main() {
add(5, 3); // Can call add() before it's defined
}
fn add(a: i32, b: i32) { // Defined after main
println!("{}", a + b);
}
Rust resolves function definitions at compile time, so order doesn't matter.
Parameters and Return Types
Adding Parameters
Parameters tell a function what data it needs:
fn add(a: i32, b: i32) {
println!("Result: {}", a + b);
}
fn main() {
add(5, 3);
add(10, 20);
}
Parameter syntax:
fn name(param1: Type, param2: Type) { }
Important: Type annotations are required for function parameters. The compiler can't infer them (unlike with let).
Common mistake:
fn add(a, b) { } // ❌ ERROR: missing types
fn add(a: i32, b: i32) { } // ✅ Correct
Returning Values
Functions can return values to the caller:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let result = add(5, 3);
println!("Result: {}", result); // 8
}
Return type syntax:
fn name() -> ReturnType { body }
The arrow -> specifies what type the function returns.
The Semicolon Trap ⚠️
This is the most important concept in this section:
// Function 1: No semicolon — returns i32
fn example() -> i32 {
5
}
// Function 2: With semicolon — returns ()
fn example() -> i32 {
5; // ❌ ERROR: expected i32, found ()
}
The rule:
- Without semicolon: Expression evaluates to a value (returns it)
- With semicolon: Statement completes, no value returned
fn returns_five() -> i32 {
5 // ✅ Returns 5
}
fn returns_nothing() {
5; // Statement, not returned
} // Implicitly returns ()
Why this matters:
- One character (
;) changes the return type - Compiler catches this at compile time
- This is why Rust emphasizes expressions over statements
Early Returns
Use return to exit a function early:
fn check_positive(n: i32) -> bool {
if n < 0 {
return false; // Exit early
}
true // Implicit return
}
fn main() {
println!("{}", check_positive(5)); // true
println!("{}", check_positive(-3)); // false
}
Functions with No Return Value
fn print_twice(text: &str) {
println!("{}", text);
println!("{}", text);
}
fn main() {
print_twice("Hello");
}
Functions without -> return () (unit type, "nothing").
Statements vs. Expressions
Rust distinguishes between statements (which don't return values) and expressions (which do). This is fundamental to Rust's design.
Statements
Statements are instructions that perform an action but don't return a value:
let x = 5; // Declaration statement
let y = (let z = 6); // ❌ ERROR: statements don't return values
Statements end with a semicolon.
Expressions
Expressions evaluate to a value you can use:
5 + 6 // Evaluates to 11
{
let x = 3;
x + 1 // Evaluates to 4 (no semicolon!)
}
Expressions do not end with a semicolon.
The Consequence
fn example() -> i32 {
5 // Expression: returns 5 ✅
}
fn example() -> i32 {
5; // Statement: returns (), compiler error ❌
}
Compiler error:
error[E0308]: mismatched types
expected: i32
found: ()
This one semicolon is easy to miss but causes real bugs!
Using Expressions in Assignments
let x = 5 + 6; // 11
let y = {
let x = 3;
x + 1 // No semicolon!
}; // y = 4
let z = {
let x = 3;
x + 1; // Semicolon!
}; // z = () — not what we want!
Control Flow: if/else
Decision-making in Rust uses if, else if, and else.
Basic if/else
fn check_number(n: i32) {
if n > 0 {
println!("Positive");
} else if n < 0 {
println!("Negative");
} else {
println!("Zero");
}
}
fn main() {
check_number(5); // Positive
check_number(-3); // Negative
check_number(0); // Zero
}
Key points:
- Condition must be a
bool(Rust won't auto-convert) - Parentheses around the condition are optional
- Braces are required (unlike C)
else ifchains for multiple conditions
The Boolean Requirement
if 5 { } // ❌ ERROR: expected bool, found i32
if 5 > 0 { } // ✅ Correct: 5 > 0 is true
if x == 5 { } // ✅ Correct: == returns bool
Common mistake: confusing = (assignment) with == (comparison):
if x = 5 { } // ❌ ERROR: assigns 5, doesn't compare
if x == 5 { } // ✅ Correct: compares x to 5
if as an Expression
if can return a value! This is powerful:
fn main() {
let number = 6;
// if as a statement
if number % 2 == 0 {
println!("Even");
} else {
println!("Odd");
}
// if as an expression
let message = if number % 2 == 0 { "even" } else { "odd" };
println!("Number is: {}", message);
}
When using if as an expression:
- Both branches must return the same type
- Each branch can be a single expression or a block
let age = 15;
let status = if age >= 18 {
"adult"
} else {
"minor"
};
Type mismatch error:
let x = if true { 5 } else { "hello" }; // ❌ int vs string
Rust requires both branches to be compatible.
Loops
Rust provides three looping constructs, each with a purpose.
while Loop
Repeat while a condition is true:
fn main() {
let mut count = 0;
while count < 5 {
println!("Count: {}", count);
count += 1;
}
}
Output:
Count: 0
Count: 1
Count: 2
Count: 3
Count: 4
When to use: When the exit condition is complex or data-dependent.
for Loop
Iterate over a range or collection:
fn main() {
// Iterate over a range
for i in 0..5 {
println!("i: {}", i);
}
// Iterate over a collection
let numbers = vec![10, 20, 30];
for num in numbers {
println!("num: {}", num);
}
}
Output:
i: 0
i: 1
i: 2
i: 3
i: 4
num: 10
num: 20
num: 30
Range syntax:
0..5— 0 to 4 (excludes 5)0..=5— 0 to 5 (includes 5)(0..5).rev()— reversed (5 down to 0)
for i in 0..5 { // 0, 1, 2, 3, 4
println!("{}", i);
}
for i in 0..=5 { // 0, 1, 2, 3, 4, 5
println!("{}", i);
}
for i in (0..5).rev() { // 4, 3, 2, 1, 0
println!("{}", i);
}
When to use: Most of the time! Rust developers prefer for over while.
Infinite Loop with break
fn main() {
let mut count = 0;
loop {
println!("Count: {}", count);
count += 1;
if count == 5 {
break; // Exit the loop
}
}
}
When to use: Server loops, game loops, or when you need explicit control.
break and continue
Control loop flow:
fn main() {
for i in 0..10 {
if i == 3 {
continue; // Skip to next iteration
}
if i == 7 {
break; // Exit loop entirely
}
println!("{}", i); // Prints: 0, 1, 2, 4, 5, 6
}
}
continue— jump to next iterationbreak— exit the loop immediately
Loop Labels (Advanced)
For nested loops, label them to control which loop to break/continue:
fn main() {
'outer: for x in 0..3 {
'inner: for y in 0..3 {
if x == 1 && y == 1 {
break 'outer; // Break out of outer loop
}
println!("({}, {})", x, y);
}
}
}
Use rarely — usually a sign you should extract a function.
match Expressions
match is Rust's pattern-matching construct. Think of it as a more powerful switch statement.
Basic Pattern Matching
fn describe_number(n: i32) {
match n {
0 => println!("Zero"),
1 => println!("One"),
2 => println!("Two"),
_ => println!("Something else"),
}
}
fn main() {
describe_number(0); // Zero
describe_number(1); // One
describe_number(5); // Something else
}
Key points:
- Each pattern has an arm with
=> _is the catch-all (matches anything not matched above)- Compiler ensures all cases are covered (exhaustiveness)
matchreturns a value (it's an expression!)
match with Ranges
fn grade_score(score: i32) -> char {
match score {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
60..=69 => 'D',
_ => 'F',
}
}
fn main() {
println!("95: {}", grade_score(95)); // A
println!("75: {}", grade_score(75)); // C
println!("45: {}", grade_score(45)); // F
}
match with Tuples
fn describe_position(x: i32, y: i32) {
match (x, y) {
(0, 0) => println!("Origin"),
(0, _) => println!("On Y-axis"),
(_, 0) => println!("On X-axis"),
_ => println!("Somewhere else"),
}
}
fn main() {
describe_position(0, 0); // Origin
describe_position(0, 5); // On Y-axis
describe_position(3, 0); // On X-axis
describe_position(2, 3); // Somewhere else
}
The underscore _ means "any value — I don't care."
match as Expression
fn main() {
let number = 3;
let message = match number {
1 => "One",
2 => "Two",
3 => "Three",
_ => "Other",
};
println!("{}", message); // Three
}
All branches must return the same type.
Why match Matters
match is how Rust handles error codes and optional values. You'll see it constantly in real code, especially with Result and Option. We'll dive deep in Howto Rust Error Handling.
Worked Examples
Let's build from simple to complex.
Example 1: Temperature Converter
Convert Celsius to Fahrenheit:
fn celsius_to_fahrenheit(c: f64) -> f64 {
(c * 9.0 / 5.0) + 32.0
}
fn main() {
let temps = vec![0.0, 10.0, 20.0, 30.0];
for celsius in temps {
let fahrenheit = celsius_to_fahrenheit(celsius);
println!("{:.1}°C = {:.1}°F", celsius, fahrenheit);
}
}
Compile and run:
rustc example.rs
./example
Output:
0.0°C = 32.0°F
10.0°C = 50.0°F
20.0°C = 68.0°F
30.0°C = 86.0°F
Concepts: Function parameters, return types, for loop, formatting.
Example 2: Grading System
Assign letter grades based on score:
fn get_grade(score: i32) -> char {
match score {
90..=100 => 'A',
80..=89 => 'B',
70..=79 => 'C',
60..=69 => 'D',
_ => 'F',
}
}
fn main() {
let students = vec![
("Alice", 95),
("Bob", 85),
("Carol", 75),
("David", 60),
("Eve", 45),
];
for (name, score) in students {
let grade = get_grade(score);
println!("{}: {} → {}", name, score, grade);
}
}
Output:
Alice: 95 → A
Bob: 85 → B
Carol: 75 → C
David: 60 → D
Eve: 45 → F
Concepts: match with ranges, tuple iteration, pattern matching in for loop.
Example 3: FizzBuzz
The classic interview problem:
fn fizzbuzz(n: i32) -> String {
match (n % 3, n % 5) {
(0, 0) => String::from("FizzBuzz"),
(0, _) => String::from("Fizz"),
(_, 0) => String::from("Buzz"),
_ => n.to_string(),
}
}
fn main() {
for i in 1..=15 {
println!("{}", fizzbuzz(i));
}
}
Output:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
Concepts: Tuple pattern matching, String conversion (to_string()), modulo operator.
Example 4: Factorial (Recursion)
Recursive functions call themselves:
fn factorial(n: u32) -> u32 {
match n {
0 | 1 => 1, // 0 or 1
n => n * factorial(n - 1), // Recursive case
}
}
fn main() {
for i in 0..=6 {
println!("{}! = {}", i, factorial(i));
}
}
Output:
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
Concepts: Recursion, pattern alternatives with |, match in functions.
Note: Rust can optimize tail recursion, but stack depth still matters. For large numbers, use loops or iterative approaches.
Common Mistakes and Compiler Errors
Mistake 1: Semicolon in Return
fn get_five() -> i32 {
5; // ❌ Returns (), not i32
}
Error:
error[E0308]: mismatched types
expected: `i32`
found: `()`
Fix: Remove the semicolon.
Mistake 2: Missing Return Type
fn add(a: i32, b: i32) { // ❌ Missing -> Type
a + b
}
Error:
error: this function's return type is not `()`
|
2 | fn add(a: i32, b: i32) {
| ^ ...is not `()`
Fix: Add -> i32 to the signature.
Mistake 3: Non-Boolean Condition
if 5 { // ❌ i32 is not bool
println!("Five");
}
Error:
error[E0308]: mismatched types
expected: `bool`
found: `i32`
Fix: Use a boolean expression.
Mistake 4: Type Mismatch in if Expression
let x = if true { 5 } else { "hello" }; // ❌ int vs string
Error:
error[E0308]: `if` and `else` have incompatible types
expected: `i32`
found: `&str`
Fix: Make both branches return the same type.
Mistake 5: Missing break in match
match value {
0 => println!("Zero"),
1 => println!("One"), // Falls through to next arm!
2 => println!("Two"),
_ => { },
}
In Rust, match arms don't fall through (unlike C's switch). Each arm must be complete.
Quick Reference
| Construct | Syntax | Purpose |
|---|---|---|
| Function | fn name(p1: T1, p2: T2) -> RetType { body } | Define reusable code |
| Parameters | (name: Type, ...) | Pass data to functions |
| Return type | -> Type | Specify what function returns |
| if expression | if cond { x } else { y } | Make decisions, return value |
| while loop | while cond { body } | Repeat while condition true |
| for loop | for var in 0..10 { body } | Iterate over ranges/collections |
| Infinite loop | loop { if done { break; } } | Loop forever until break |
| break | break; | Exit loop immediately |
| continue | continue; | Jump to next iteration |
| match | match val { 0 => x, _ => y } | Pattern matching |
| Range excl. | 0..5 | 0, 1, 2, 3, 4 |
| Range incl. | 0..=5 | 0, 1, 2, 3, 4, 5 |
Key Takeaways
- Functions are everywhere — use them to organize and reuse code
- Type annotations required for function parameters (unlike
let) - The semicolon trap — without
;it's an expression (returns value), with;it's a statement (doesn't) - Expressions over statements — Rust treats more things as expressions than C
forloops are idiomatic — Rust devs prefer them overwhilematchis powerful — it's how you handle decisions in Rust- Compiler is helpful — it won't let you compile broken code; listen to error messages
Next Steps
Level Up Your Skills
- Ownership & Borrowing — Functions pass data using ownership rules; read Howto Rust Ownership Borrowing to understand how
- Error Handling — Use
matchwithResultto handle failures gracefully - Collections — Combine functions and loops to build useful programs with Howto Rust Collections
- Concepts Demo — See how functions and control flow work together in Howto Rust Concepts Demo
Practice Problems
Try these to reinforce what you learned:
- Fibonacci — Recursive or iterative
- Prime Checker — Write
is_prime(n: u32) -> bool - Word Counter — Iterate over a string, count words
- Average Calculator — Take a
Vec<i32>, return average asf64 - Reverse String — Take
&str, return reversedString
Resources
Official Documentation
Interactive Learning
- Rust Playground — Try code in your browser
- Rustlings — Interactive exercises
Last Updated: April 7, 2026
Author: CLAW-02
Category: Tutorial / How-To
Difficulty: Beginner-Intermediate
Prerequisites: Howto Rust Getting Started
Time to complete: 45-60 minutes of reading + practice
References:
- Previous: Howto Rust Getting Started
- Next: Howto Rust Ownership Borrowing
- Companion: Howto Rust Error Handling
- Integration: Howto Rust Concepts Demo