Learn how functions work in Rust and master its expressive control flow constructs.
Rust is an expression-based language. This single fact changes how you write code in ways that feel strange at first, then become second nature.
Let me show you what I mean.
fn main() {
let y = {
let x = 3;
x + 1 // No semicolon = expression, returns value
};
println!("The value of y is: {y}"); // Prints: 4
}
That block { let x = 3; x + 1 } evaluates to 4. The entire block is an expression. This is different from most languages you have used.
Statements perform an action and return nothing. Expressions evaluate to a value. In Rust, almost everything is an expression - including blocks, if statements, and loops.
Here is the critical rule: adding a semicolon turns an expression into a statement.
let x = 3; // Statement (declaration)
x + 1 // Expression (returns 4)
x + 1; // Statement (returns nothing)
Why does this matter? Because unlike many other languages, you cannot do this in Rust:
// This works in C, JavaScript, Python...
let x = y = 4; // ERROR in Rust!
Assignment is a statement in Rust, not an expression. It returns () (the unit type), not the assigned value.
The difference between x + 1 and x + 1; is whether your function returns a value or returns nothing. A single character determines your return type.
fn add(a: i32, b: i32) -> i32 {
a + b // Implicit return - no semicolon!
}
fn print_sum(a: i32, b: i32) {
println!("Sum: {}", a + b);
// No return value, no -> in signature
}
Rust functions use fn, require type annotations on parameters, and specify return types after ->. Notice add has no return keyword - the last expression becomes the return value automatically.
Use explicit return only for early returns. For the final value, omit the semicolon and let the expression be the implicit return. This is idiomatic Rust.
You can use return explicitly, but most Rustaceans reserve it for early exits:
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
return 0.0; // Early return with explicit keyword
}
a / b // Implicit return for the "normal" case
}
Does this line return a value: `let x = 5;`
Since if is an expression in Rust, you can use it on the right side of a let:
let number = 6;
let description = if number % 2 == 0 { "even" } else { "odd" };
println!("{number} is {description}");
No ternary operator needed. The if expression handles it elegantly.
All arms of an if expression must return the same type. You cannot return a string from one branch and an integer from another. Rust checks this at compile time.
// This will NOT compile
let value = if condition { 5 } else { "six" };
// ERROR: expected integer, found &str
What happens when there is no else? If the condition is false and there is no else branch, the expression returns () (unit type). This means the if branch must also return ():
// This works - both branches return ()
let x = 5;
if x > 3 {
println!("big");
}
// This does NOT work
let result = if x > 3 { 10 }; // ERROR: else branch returns ()
Rust has three loop constructs, each with distinct use cases.
loop creates an infinite loop that you break out of explicitly. The powerful part: break can return a value.
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Returns 20 from the loop!
}
};
println!("Result: {result}"); // Prints: Result: 20
This is unique to loop. Neither while nor for can break with a value.
Only loop guarantees execution of the loop body at least once (if reached). The compiler knows a break with a value will eventually execute. With while and for, the body might never run, making return values impossible to guarantee.
When you have nested loops, use labels to control which loop break or continue affects:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break; // Exits inner loop only
}
if count == 2 {
break 'counting_up; // Exits outer loop
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
Labels start with a single quote ('counting_up). They let you break out of deeply nested loops without boolean flags or other workarounds.
Use while when you want to loop until a condition becomes false:
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!");
}
Unlike loop, you cannot use break value; in a while loop. The compiler cannot guarantee the loop body executes, so it cannot guarantee a return value.
for is the most common loop in Rust. It iterates over collections and ranges:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("Value: {element}");
}
}
For counting or iterating numbers, use ranges:
// Prints 1, 2, 3 (exclusive end)
for number in 1..4 {
println!("{number}");
}
// Prints 1, 2, 3, 4 (inclusive end)
for number in 1..=4 {
println!("{number}");
}
// Countdown: 3, 2, 1
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!");
1..4 is exclusive (1, 2, 3). 1..=4 is inclusive (1, 2, 3, 4). The = makes it include the end value. Use .rev() to reverse any range.
Consider iterating over an array. You could use while:
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("Value: {}", a[index]);
index += 1;
}
But this has problems. If you change the array size and forget to update 5, you get a panic at runtime. With for, this cannot happen:
let a = [10, 20, 30, 40, 50];
for element in a {
println!("Value: {element}");
}
The for loop handles bounds automatically. It is safer, clearer, and more idiomatic.
Let us combine these concepts. Here is a function that demonstrates Rust's expression-based nature:
fn classify_number(n: i32) -> &'static str {
if n < 0 {
"negative"
} else if n == 0 {
"zero"
} else if n % 2 == 0 {
"positive even"
} else {
"positive odd"
}
}
fn sum_range(start: i32, end: i32) -> i32 {
let mut total = 0;
for n in start..=end {
total += n;
}
total // Implicit return
}
fn main() {
println!("{}", classify_number(7)); // "positive odd"
println!("Sum 1-10: {}", sum_range(1, 10)); // 55
}
Notice how classify_number has no return keywords. Each branch of the if is an expression that becomes the return value. This is clean, idiomatic Rust.
In Rust's range syntax, what would iterate through the numbers 1 through 100 inclusive?
Rust's expression-based design changes how you write code:
{ let x = 3; x + 1 } evaluates to 4x + 1 returns a value, x + 1; does notbreak value; exits and returns1..10 (exclusive) or 1..=10 (inclusive)This expression-oriented style leads to cleaner code with fewer intermediate variables. It takes adjustment, but soon you will find it hard to go back to statement-heavy languages.
Next, we will explore why memory management matters and set the stage for Rust's most distinctive feature: ownership.