Knowing that blocks are expressions explains Rust's missing return statements and stray-semicolon errors.
Functions are declared with fn, parameters always carry explicit types, and the return type follows ->. Type inference works inside function bodies but never across signatures, which keeps signatures readable as documentation.
Rust is expression-oriented. A block { ... } evaluates to its last expression, if that expression has no trailing semicolon. That is how functions return values without return:
fn square(x: i32) -> i32 {
x * x
}Adding a semicolon (x * x;) turns the expression into a statement, the block then evaluates to (), and you get a "mismatched types: expected i32, found ()" error. Recognise this one; everyone hits it.
return still exists and is used for early exits, for example a guard at the top of a function. if, match and blocks are expressions too, so you can write let label = if n > 0 { "pos" } else { "non-pos" };. Both branches must then have the same type.
A function with no -> returns (). A function that never returns, such as one that always panics or loops forever, can be declared -> ! (the never type).
Function names use snake_case. Order does not matter: you can call a function defined further down the file. Rust has no default arguments or overloading; use different names, an Option parameter, or a builder struct instead.
Example
fn area(width: f64, height: f64) -> f64 {
width * height // no semicolon: this is the return value
}
fn classify(n: i32) -> &'static str {
if n < 0 {
return "negative"; // early return
}
let kind = if n % 2 == 0 { "even" } else { "odd" };
kind
}
fn main() {
let total = {
let a = area(2.0, 3.5);
a + 1.0 // block evaluates to 8.0
};
println!("{total} {} {}", classify(-3), classify(4)); // 8 negative even
}Remember
- Parameter and return types are always written out
- The last expression of a block, without
;, is its value returnis for early exits;ifandmatchare expressions
Watch out for
- Ending the final line with
;and gettingexpected i32, found ().