Rust Semicolons and Block Values Explained
In Rust, a block’s value is the value of its final expression, and a semicolon changes that expression into a statement that produces no value. So adding or removing a semicolon at the end of a block can change whether the block returns a useful value or returns the unit value, written ().
Expressions and statements in Rust
Rust distinguishes expressions, which evaluate to values, from statements, which perform an action without supplying a value to the surrounding code. This distinction explains why punctuation that looks minor can affect a function’s return value or whether code type-checks.
A literal such as 7 is an expression. A variable declaration such as let answer = 7; is a statement. The declaration ends with a semicolon, but the expression on its right-hand side still provides the value assigned to answer.
Many familiar constructs are expressions in Rust: a function call, an arithmetic operation, an if expression, and a block can all produce values. Expressions may also be used as statements when their results are not needed. The semicolon is one way to mark that the result is being discarded.
How a block gets its value
A block is a sequence of statements followed, optionally, by a final expression. Its value is the value of that final expression. Braces create a new block scope, so the final expression can be used where the block itself appears.
let number = {
let base = 4;
base + 3
};
println!("{number}"); // 7
Here, let base = 4; is a statement. The last line, base + 3, is an expression without a semicolon. Its value, 7, becomes the value of the block and is assigned to number.
The braces do not automatically make the block return the last thing written. The last thing must be an expression rather than a semicolon-terminated statement if its value is to become the block’s value.
What the semicolon changes
Compare the block above with this version:
let result = {
let base = 4;
base + 3;
};
The semicolon after base + 3 turns that expression into a statement and discards its result. The block therefore evaluates to (), Rust’s unit value. In this example, result has type (), not an integer.
This is not a special rule that applies only to the final line of a block. A semicolon after an expression means its value is not the value of the enclosing block. When the expression is followed by more code, it is typically being used as a statement anyway. The important case is the last item: with no semicolon, it can supply the block’s value; with a semicolon, it does not.
{ 12 }evaluates to the integer12.{ 12; }evaluates to().{ let x = 12; x }evaluates to12.{ let x = 12; x; }evaluates to().
Blocks, functions, and return values
A function body is itself a block. If its final expression has no semicolon, that expression supplies the function’s return value, provided its type matches the return type declared in the signature.
fn double(n: i32) -> i32 {
n * 2
}
The final expression, n * 2, returns an i32. Adding a semicolon changes the body’s value to ():
fn double(n: i32) -> i32 {
n * 2;
}
This does not satisfy the function’s declared return type, so Rust reports a type mismatch. The compiler is not saying the multiplication failed; it is saying that the function body evaluates to unit where an i32 is required.
Rust also supports explicit early returns, such as return n * 2;. The semicolon after an explicit return is normal because the return expression exits the function rather than serving as the block’s implicit final value. For ordinary functions, leaving the final expression without a semicolon is a common and idiomatic way to return it.
Using blocks in conditionals and other expressions
Because blocks produce values, they can appear inside larger expressions. In an if expression, each branch that may be selected must produce a compatible type:
let label = if score >= 50 {
"pass"
} else {
"retry"
};
Both branches end in string literals without semicolons, so each block produces a string slice and the whole if expression can initialize label. If a semicolon is added after "pass", that branch instead evaluates to (). The branches then have incompatible types, and Rust rejects the expression.
The same principle applies to blocks used in assignments, function arguments, and match arms: the expression in that position must provide the type the surrounding code expects. A semicolon is appropriate when the result is intentionally ignored, not when the surrounding expression needs it.
Common mistakes and compiler clues
A frequent beginner mistake is adding a semicolon to every line, as in languages where statement-ending punctuation is routine. Rust permits many such semicolons, but a semicolon after the intended final expression changes the block’s result. If a compiler error mentions an expected value such as i32 but found (), inspect the end of the relevant block first.
Another common misunderstanding is thinking that a semicolon makes a function call happen. The call happens whether or not its result is discarded. For example, println!("hello"); is conventionally written with a semicolon because it is used for its side effect and does not provide a useful value for the surrounding block.
- Check the final expression in the function or block whose type is reported.
- Remove its semicolon if its value should be returned or passed outward.
- Keep the semicolon if the result is intentionally unused and the block should evaluate to
(). - For an
iformatchexpression, check that all relevant branches produce compatible types.
Do not remove semicolons indiscriminately. A declaration such as let count = 3; is a statement and normally ends with one. Rust’s rule is about whether an expression’s value is the value of its enclosing block, not about removing punctuation everywhere.
Frequently Asked Questions
Does a semicolon always make a Rust expression return nothing?
A semicolon discards an expression’s value in that statement position. The enclosing block then evaluates to () if that semicolon-terminated statement is its final item and there is no later final expression.
Why does Rust say expected an integer but found ()?
That often means a block or function body ends with a semicolon-terminated expression. The expression’s result was discarded, leaving the block with unit type instead of the required integer type.
Why does println! usually have a semicolon?
It is generally used for its printing side effect, not for a value to pass to the next expression. Ending the invocation with a semicolon makes that intent clear and yields unit for the statement.
Can I omit the semicolon after a let declaration?
No. A let declaration is a statement, and Rust requires it to end with a semicolon. To make a block return a value, put a separate expression after the declaration and omit the semicolon on that final expression.
Is the final expression of every Rust function its return value?
For a function that reaches the end of its body normally, the body’s final expression supplies its implicit return value. Its type must match the function’s declared return type; an explicit return can exit earlier.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.