Functions & Expressions
Rust is expression-oriented: most constructs evaluate to a value. Functions, blocks, if, and match are expressions. Statements (let, ;-terminated lines) perform actions without producing outer values.
Recipe
fn square(n: i32) -> i32 {
n * n
}
fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
fn main() {
let result = {
let x = 4;
square(x) + 1
};
println!("{result}");
}When to reach for this: Structuring logic into small, composable functions and using blocks to scope intermediate values.
Working Example
fn classify(score: u32) -> &'static str {
match score {
0..=49 => "fail",
50..=79 => "pass",
_ => "honors",
}
}
fn report(scores: &[u32]) -> String {
let mut lines = String::new();
for (i, &s) in scores.iter().enumerate() {
let label = classify(s);
lines.push_str(&format!("student {}: {label}\n", i + 1));
}
lines
}
fn main() {
println!("{}", report(&[88, 42, 65]));
}What this demonstrates:
matchranges as an expression returning&str- Block expression in
reportbuilding aString - Slice parameter
&[u32]accepts arrays andVec - Functions compose without classes or methods (methods come later)
Deep Dive
How It Works
- Function signatures require parameter types; return type after
-> - The function body is a block expression
return expr;exits early; trailing expression omitsreturn- A semicolon after the last expression returns
()instead of the value
Statements vs Expressions
let x = 5; // statement
5 + 1 // expression, evaluates to 6
{ let y = 3; y + 1 } // block expression -> 4Diverging Functions
fn die(msg: &str) -> ! {
panic!("{msg}");
}Return type ! means the function never returns (panic, loop, exit).
Closures Preview
let add_one = |x| x + 1;
let doubled: Vec<_> = [1, 2, 3].iter().map(|n| n * 2).collect();Closures capture variables from their environment. Full coverage is in the Closures section.
Gotchas
- Semicolon on the last line -
fn id(x: i32) -> i32 { x; }returns()and fails to compile. Fix: Remove the trailing;. - Mismatched
ifarm types -if cond { 1 } else { "two" }errors. Fix: Return the same type from all arms or use explicit coercion. - Returning references to locals -
fn bad() -> &str { let s = String::from("x"); &s }fails borrow check. Fix: Return ownedStringor take input by reference. - Shadowing parameter names - Legal but confusing. Fix: Use distinct names inside the body.
- Overusing
return- Scatteredreturnhides the natural expression flow. Fix: Use earlyreturnonly for guards; otherwise use a final expression.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Closure | Short callback passed to iterator | Logic reused in many places |
| Method on struct | Behavior tied to data | Free functions suffice for utilities |
| Macro | Repeated syntax patterns | A plain function works |
const fn | Compile-time evaluation needed | Runtime-only logic |
FAQs
Can functions return multiple values?
Return a tuple, struct, or use out-parameters via &mut. Tuples are fine for 2-3 related values.
What is the unit type `()`?
The empty tuple. main can return () or Result / ExitCode in binaries.
Are parameters immutable by default?
Yes. Use mut param only if you reassign the local binding inside the function.
Can I overload functions?
No. Different names or generic traits provide polymorphism instead.
What is an expression-oriented `if`?
let v = if cond { a } else { b }; assigns the branch result to v.
How do I pass a function pointer?
fn(i32) -> i32 is a function pointer type. Closures with captures need Fn traits or Box<dyn Fn>.
Can blocks contain statements and still be expressions?
Yes. Only the final expression (without ;) becomes the block's value.
What is tail recursion status?
Rust does not guarantee tail-call optimization. Prefer loops for deep recursion.
How do I document functions?
Use /// doc comments above the item. They support Markdown and appear in cargo doc.
Can `main` return `Result`?
Yes. fn main() -> Result<(), Box<dyn std::error::Error>> propagates errors with ?.
Related
- Control Flow - loops and labeled blocks
- Rust Basics - first functions tour
- Closures Basics - anonymous functions
- Traits Basics - behavior as traits
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.