Returning Expressions
In Rust, every function body is a block expression { ... }. When the final expression in a function body omits a trailing semicolon, it becomes the block's tail expression, and its evaluated value is automatically returned to the caller without writing the return keyword.
Tail Expressions as Return Values
A function executes its statements sequentially from top to bottom and returns the value produced by its final expression:
fn rectangle_area(width: u32, height: u32) -> u32 {
width * height
}
fn fahrenheit_to_celsius(f: f64) -> f64 {
let shifted = f - 32.0;
shifted * 5.0 / 9.0
}
fn main() {
println!("Area: {}", rectangle_area(8, 5));
println!("Celsius: {:.1}", fahrenheit_to_celsius(98.6));
}
Area: 40
Celsius: 37.0
In fahrenheit_to_celsius, let shifted = f - 32.0; is a declaration statement terminated by a semicolon, and shifted * 5.0 / 9.0 is the tail expression whose f64 value becomes the function's return value.
fn fahrenheit_to_celsius(f: f64) -> f64 {
let shifted = f - 32.0; <-- Statement (ends with `;`)
shifted * 5.0 / 9.0 <-- Tail expression (no `;`) -> Returned as f64
}
Tail Expression vs return Statement
Writing shifted * 5.0 / 9.0 as the tail expression and writing return shifted * 5.0 / 9.0; at the end of a function compile to identical machine code. However, idiomatic Rust uses bare tail expressions for the final return value of a function and reserves the return keyword for early exits from the middle of a function.
The Clippy linter enforces this convention via the needless_return lint:
| Position in Function | Idiomatic Syntax | Why |
|---|---|---|
| Final expression of the function body | expression (no return, no ;) |
Treats the function body naturally as a value-producing block |
Guard clause or early exit inside an if/loop |
return expression; |
Explicitly signals non-linear control flow that skips the rest of the function |
Returning Control-Flow and Block Expressions
Because if, match, loop, and nested { ... } blocks are expressions in Rust, any of them can serve directly as a function's tail expression.
Returning an if / else Expression
When an if / else expression is the final item in a function, the tail expression of whichever branch executes becomes the return value of the entire function:
fn clamp(value: i32, min: i32, max: i32) -> i32 {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
fn main() {
println!("clamp(-5, 0, 10) = {}", clamp(-5, 0, 10));
println!("clamp(4, 0, 10) = {}", clamp(4, 0, 10));
println!("clamp(25, 0, 10) = {}", clamp(25, 0, 10));
}
clamp(-5, 0, 10) = 0
clamp(4, 0, 10) = 4
clamp(25, 0, 10) = 10
Notice that no semicolon appears inside any of the three branches (min, max, value), nor after the closing brace } of the else block.
Returning a match Expression
A match expression at the end of a function returns the value of the matched arm directly:
fn http_status_text(code: u16) -> &'static str {
match code {
200 => "OK",
201 => "Created",
400 => "Bad Request",
404 => "Not Found",
500..=599 => "Server Error",
_ => "Unknown Status",
}
}
fn main() {
println!("200 -> {}", http_status_text(200));
println!("503 -> {}", http_status_text(503));
}
200 -> OK
503 -> Server Error
Returning a Nested Block or loop Expression
A nested block { ... } or a loop that breaks with a value (break expr) can also be the function's tail expression:
fn next_power_of_two(target: u32) -> u32 {
let mut power = 1;
loop {
if power >= target {
break power; // Evaluates the `loop` expression to `power`
}
power *= 2;
}
}
fn main() {
println!("Next power of two >= 19: {}", next_power_of_two(19));
}
Next power of two >= 19: 32
Evaluation and Drop Order When Returning an Expression
When a function returns a tail expression, Rust follows a strict evaluation and cleanup order:
- The tail expression is evaluated first, and its resulting value is moved or copied into the caller's return slot.
- Any temporary values created while evaluating the tail expression are dropped.
- All remaining local variables and parameters owned by the function are dropped in reverse declaration order (LIFO) before control returns to the caller.
Because step 1 happens before step 3, a tail expression can compute a value derived from borrowing a local variable (such as local_string.len()), even though local_string is destroyed as the function exits:
fn count_non_empty_lines(text: &str) -> usize {
let owned_copy = text.trim().to_string();
// `owned_copy` is borrowed here to count lines, and the `usize` result is
// copied into the return slot BEFORE `owned_copy` is dropped at function exit.
owned_copy.lines().filter(|line| !line.is_empty()).count()
}
fn main() {
let sample = "\nalpha\n\nbeta\ngamma\n";
println!("Non-empty lines: {}", count_non_empty_lines(sample));
}
Non-empty lines: 3
Common Compiler Errors
Accidental Semicolon on the Tail Expression
Adding a semicolon to the final expression of a function converts it into an expression statement that discards its value and causes the function block to evaluate to ():
// ❌ does not compile
fn double(x: i32) -> i32 {
x * 2;
}
fn main() {
println!("{}", double(6));
}
error[E0308]: mismatched types
--> src/main.rs:2:22
|
2 | fn double(x: i32) -> i32 {
| ------ ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
3 | x * 2;
| - help: remove this semicolon to return this value
error: aborting due to 1 previous error
Semicolon Inside One Branch of a Tail if Expression
When an if / else expression is used as a function's tail expression, every branch must end with a value-producing tail expression of the return type. Putting a semicolon inside even one branch turns that branch's value into ():
// ❌ does not compile
fn absolute(x: i32) -> i32 {
if x >= 0 {
x
} else {
-x;
}
}
fn main() {
println!("{}", absolute(-3));
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:6:9
|
3 | / if x >= 0 {
4 | | x
| | - expected because of this
5 | | } else {
6 | | -x;
| | ^^^
| | |
| | expected `i32`, found `()`
| | help: consider removing this semicolon
7 | | }
| |_____- `if` and `else` have incompatible types
error: aborting due to 1 previous error
Returning a Reference to a Local Variable
While a function can return an owned value (String, Vec<T>, i32) computed in its tail expression (because ownership transfers to the caller), a tail expression cannot return a reference (&T) pointing to a local variable owned by the function, because that local variable is dropped when the function exits:
// ❌ does not compile
fn make_greeting(name: &str) -> &str {
let formatted = format!("Hello, {name}!");
&formatted
}
fn main() {
println!("{}", make_greeting("Ferris"));
}
error[E0515]: cannot return reference to local variable `formatted`
--> src/main.rs:4:5
|
4 | &formatted
| ^^^^^^^^^^ returns a reference to data owned by the current function
error: aborting due to 1 previous error
Return the owned String directly (formatted instead of &formatted with return type -> String) so ownership moves safely to the caller.