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.
Search across all documentation pages
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.
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.
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:
match ranges as an expression returning &strreport building a String&[u32] accepts arrays and Vec->return expr; exits early; trailing expression omits return() instead of the valuelet x = 5; // statement
5 + 1 // expression, evaluates to 6
{ let y = 3; y + 1 } // block expression -> 4fn die(msg: &str) -> ! {
panic!("{msg}");
}Return type ! means the function never returns (panic, loop, exit).
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.
fn id(x: i32) -> i32 { x; } returns () and fails to compile. Fix: Remove the trailing ;.if arm types - if cond { 1 } else { "two" } errors. Fix: Return the same type from all arms or use explicit coercion.fn bad() -> &str { let s = String::from("x"); &s } fails borrow check. Fix: Return owned String or take input by reference.return - Scattered return hides the natural expression flow. Fix: Use early return only for guards; otherwise use a final expression.| 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 |
Return a tuple, struct, or use out-parameters via &mut. Tuples are fine for 2-3 related values.
The empty tuple. main can return () or Result / ExitCode in binaries.
Yes. Use mut param only if you reassign the local binding inside the function.
No. Different names or generic traits provide polymorphism instead.
let v = if cond { a } else { b }; assigns the branch result to v.
fn(i32) -> i32 is a function pointer type. Closures with captures need Fn traits or Box<dyn Fn>.
Yes. Only the final expression (without ;) becomes the block's value.
Rust does not guarantee tail-call optimization. Prefer loops for deep recursion.
Use /// doc comments above the item. They support Markdown and appear in cargo doc.
Yes. fn main() -> Result<(), Box<dyn std::error::Error>> propagates errors with ?.
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+.
Reviewed by Chris St. John·Last updated Jul 16, 2026