A block is an expression¶
Level: 101 → 201 · for newcomers
One line: { } does two jobs — it opens a scope that ends at its closing brace, and it is an expression whose value is its last line written without a semicolon — and the second job is why a function body needs no return, why if can sit on the right of a let, and why adding one character can change a function's type.
Most people meet the first job on day one and the second one by accident, as an error message. They are worth learning together, because nearly every "wait, Rust lets you write that?" moment in a first month — a function body with no return, a let whose right-hand side is an if, a match used as a value — is the second job doing something a statement-oriented language cannot do at all.
Job 1: it opens a scope, anywhere you like¶
A block does not need a fn, an if, or a loop to attach to. Braces on their own are a perfectly good statement:
let n = 5;
{
let n = 10;
println!("inner n is: {n}"); // inner n is: 10
}
println!("outer n is: {n}"); // outer n is: 5
The inner n is a second variable that exists for three lines. At the closing brace it is gone, and the name goes back to meaning the outer one — which was never touched.
Two things worth being precise about here, because this snippet circulates as a shadowing demo and it is the least interesting kind:
- This is shadowing, but the boring kind. An inner scope hiding an outer name is the textbook definition, and C, C++, Java, JavaScript and Python all do it. What is unusual about Rust is a second
letof the same name in the same scope, which needs no braces at all — see Shadowing andunwrap, or SHADOWING.md for the whole set. This page is about the braces, not about the shadow. - A name declared inside really is gone afterwards. Not hidden, not shadowed — absent:
Job 2: it has a value¶
This is the half that is genuinely new if you are coming from C, Python, Java or ABAP. A block evaluates to something, and that something is its last line written without a semicolon:
let quorum = {
let voters = 9;
let half = voters / 2;
half + 1 // no semicolon: this is what the block is worth
};
println!("{quorum}"); // 5
voters and half are working names that never escape. What comes out is one value, bound to one immutable name. There is no helper function, no placeholder, and no mut.
That last line is called the tail expression, and everything else about Rust's syntax falls out of it.
Both jobs at once¶
Put the two together and you get the snippet that circulates as a quiz card:
Each half is one of the jobs above. let x = 3 is job 1 — a second variable, gone at the closing brace, the outer 10 untouched. x + 1 written without a semicolon is job 2 — the block's value, and therefore y.
Two semicolons are doing opposite things. The one after } terminates the let y = … statement, and every let needs it. The one missing after x + 1 is what gives the block a value to hand over. Write that one in and the shadowing is unchanged, but y is not:
let y = {
let x = 3;
x + 1; // warning: unused arithmetic operation that must be used
}; // `y` is `()` now, not 4
The usual gloss — the trailing semicolon makes it a statement, and that is what y gets — is backwards twice over. That semicolon belongs to the let, not to the block; and a block whose last line is a statement is worth (), with nothing to bind.
The semicolon is the switch¶
A semicolon turns an expression into a statement, and statements have no value — so the block's value becomes (), the unit type:
Same block, one character apart, two different types. This is not a corner case you can avoid; it is the single most common first-week compile error in Rust, because a function body is just a block:
error[E0308]: mismatched types
--> p2.rs:1:22
|
1 | fn double(n: i32) -> i32 {
| ------ ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | n * 2;
| - help: remove this semicolon to return this value
Read where the ^^^ points: at the return type, not at the semicolon. The compiler is saying "you promised i32 and this body produces ()", and the reason is four lines further down in the help:. Beginners routinely respond by changing the signature, which makes the error go away and the function useless.
The other direction has a lint of its own. return n * 2; in the tail position compiles fine, and clippy::needless_return — warn-by-default, so you do not have to opt in — will ask you to drop it:
return still earns its keep for an early exit from the middle of a function. In the last line it is noise.
Which is why if, match and loop are expressions too¶
None of these is a special form. They are built out of blocks, so they inherit blocks' value:
let verdict = if turnout >= 50 { "quorate" } else { "short" };
let label = match score {
5 => "excellent",
3..=4 => "good",
_ => "weak",
};
The if arms are blocks; "quorate" is a tail expression. This is also why an if used as a value must have an else — a missing branch would leave a path with no value — and why every match arm has to produce the same type. Both rules stop being arbitrary once you see the block underneath.
What you actually reach for it for¶
Three jobs, in rough order of how often they come up.
Scope the mut to the building. Mutation is how you build a collection; it is not something the rest of the function needs to keep the right to do.
let ballots = {
let mut v = Vec::new();
v.push(5);
v.push(3);
v.push(4);
v
};
// `ballots` is not `mut`. Nothing below this line can push to it.
Give a shadow an end. A shadow normally runs to the end of its scope; a block chooses that end deliberately — which is the standard fix for the one shadowing bug the compiler will not catch. See When to shadow, and A name is not a place for this same idiom seen from the shadow's side — including the qualifier that cuts the other way, that a same-scope shadow does not end early at all.
End a borrow early. A borrow taken inside a block cannot outlive it, so the block is the tool when you need the original writable again. Note the caveat: since Rust 2018 a borrow ends at its last use rather than at the closing brace, so most of the { } blocks you will see doing this in older code and older tutorials are no longer necessary. It is still the answer when the compiler disagrees with you about where the last use was. See Borrowing for the rule, and Scope is about names, not values for why a name's scope and a borrow's region are two different measurements that happen to share a word.
A fourth, rarer and worth knowing about: releasing a lock. A MutexGuard unlocks when it drops, and it drops at the end of its scope — so wrapping the critical section in a block is how you unlock before the function ends. Lock poisoning covers what happens when that goes wrong.
The whole thing, running¶
a_block_is_an_expression.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! A block is an expression.
//!
//! `{ }` does two jobs, and most people only ever meet the first. It opens a
//! SCOPE — a name declared inside stops existing at the closing brace. It is
//! also an EXPRESSION — its value is its last line written without a semicolon.
//! The second job is why a function body needs no `return`, why `if` can sit on
//! the right-hand side of a `let`, and why adding one semicolon changes what a
//! block is worth from an `i32` to `()`.
//!
//! rustc --edition 2024 a_block_is_an_expression.rs -o /tmp/abie && /tmp/abie
fn banner(title: &str) {
println!("\n──── {title}");
}
/// A function body IS a block, and `n * 2` is its tail expression.
fn double(n: i32) -> i32 {
n * 2
}
/// Something to call when the point is the semicolon, not the arithmetic.
fn tally() -> i32 {
6
}
fn main() {
banner("Job 1: it opens a scope, anywhere you like");
let n = 5;
println!(" outer n is: {n}");
{
let n = 10; // a second variable; the brace below ends it
println!(" inner n is: {n}");
}
println!(" outer n is: {n} <- the outer one was never touched");
banner("...and a name declared inside is gone at the brace");
println!(" {{ let a = 1; }}");
println!(" println!(\"{{a}}\"); <- error[E0425]: cannot find value `a`");
println!(" No subtlety: past the brace, the name does not exist.");
banner("Job 2: it has a VALUE — its last line, with no semicolon");
let quorum = {
let voters = 9;
let half = voters / 2;
half + 1 // no semicolon: this is what the block is worth
};
println!(" quorum = {quorum}");
banner("Both jobs at once — the snippet that circulates as a quiz");
let x = 10;
let y = {
let x = 3; // job 1: a second `x`, ending at the brace below
x + 1 // job 2: no semicolon, so this is what `y` is worth
};
println!(" x: {x}, y: {y}");
println!(" Two semicolons, two jobs: the one after `}}` ends the `let`,");
println!(" and the one MISSING after `x + 1` is what gives `y` a value.");
banner("The semicolon is the switch");
let with_tail = { tally() };
let with_semi = { tally(); };
println!(" {{ tally() }} is {with_tail}");
println!(" {{ tally(); }} is {with_semi:?} <- the unit value");
println!(" Same block, one character apart, two different types.");
banner("So a function body was a block all along");
println!(" fn double(n: i32) -> i32 {{ n * 2 }}");
println!(" double(4) = {} <- a tail expression, not a `return`", double(4));
banner("What it is FOR (1): scoping the `mut` to the building");
let ballots = {
let mut v = Vec::new();
v.push(5);
v.push(3);
v.push(4);
v // hand the finished Vec out; the `mut` stays behind
};
println!(" ballots = {ballots:?}");
println!(" `ballots` is not `mut`, and no line below here can grow it.");
banner("What it is FOR (2): giving a shadow an end");
let name = String::from("ada");
{
let name = name.to_uppercase();
println!(" inside: {name}");
}
println!(" outside: {name} <- the shadow ended at the brace");
banner("What it is FOR (3): the branch that decides a value");
let turnout = 61;
let verdict = if turnout >= 50 { "quorate" } else { "short" };
println!(" turnout {turnout}% -> {verdict}");
println!(" `if` is an expression because its arms are blocks.");
}
Verified output of a_block_is_an_expression.rs — regenerated by tools/run_examples.py, never hand-typed.
──── Job 1: it opens a scope, anywhere you like
outer n is: 5
inner n is: 10
outer n is: 5 <- the outer one was never touched
──── ...and a name declared inside is gone at the brace
{ let a = 1; }
println!("{a}"); <- error[E0425]: cannot find value `a`
No subtlety: past the brace, the name does not exist.
──── Job 2: it has a VALUE — its last line, with no semicolon
quorum = 5
──── Both jobs at once — the snippet that circulates as a quiz
x: 10, y: 4
Two semicolons, two jobs: the one after `}` ends the `let`,
and the one MISSING after `x + 1` is what gives `y` a value.
──── The semicolon is the switch
{ tally() } is 6
{ tally(); } is () <- the unit value
Same block, one character apart, two different types.
──── So a function body was a block all along
fn double(n: i32) -> i32 { n * 2 }
double(4) = 8 <- a tail expression, not a `return`
──── What it is FOR (1): scoping the `mut` to the building
ballots = [5, 3, 4]
`ballots` is not `mut`, and no line below here can grow it.
──── What it is FOR (2): giving a shadow an end
inside: ADA
outside: ada <- the shadow ended at the brace
──── What it is FOR (3): the branch that decides a value
turnout 61% -> quorate
`if` is an expression because its arms are blocks.
If you are coming from another language¶
- Python. Neither job transfers. An
iforforbody is not a scope — a name bound inside it is still there afterwards, which is why Python needsnonlocalandglobaland Rust does not. And a block has no value:a if c else band the walrus:=exist precisely because there is no way to give a multi-statement block one, so anything needing several steps becomes a helper function. In Rust the braces do that job, and the working names stay inside. - ABAP. Also neither.
DATAis routine-wide, soIF … ENDIFintroduces no scope at all and a name declared inside a loop is visible after it — thelv_/lt_prefixes exist partly because every local in a routine shares one namespace. There is no block expression either: a value built in several steps needs a routine-level variable or its ownFORM/method. What you gain is that a working name genuinely stops existing; what you give up is ABAP's free guarantee that one name in a routine means one thing. - C and C++. Job 1 transfers exactly — block scope, nesting, the lot. Job 2 does not: a C block has no value, which is why GCC and Clang ship
({ … })statement expressions as a non-standard extension, and why macros that need several steps depend on it. Rust's version is the ordinary language, not an extension. - JavaScript. Job 1 transfers if you use
let/const(varis function-scoped and ignores blocks). Job 2 does not, and the workaround is the IIFE —(() => { … })()— which is exactly what a Rust block expression replaces, minus the function call and the closure. - Java. Block scope yes, block value no. A
switchexpression arrived in Java 14 withyieldfor the multi-statement case, which is Java retrofitting the tail expression onto one construct; Rust has it on all of them because it never had the split.
Traps¶
- Changing the signature to silence
E0308. The error points at the return type because that is what was promised. The cause is thehelp:line — a semicolon. Fix the body. - Deleting the tail line's neighbour and taking the tail with it. You get caught two different ways, which is lucky:
{ let x = 5; }compiles as()and warnsunused variable, while{ let x = 5 }is a syntax error —expected one of ., ;, ?, else, or an operator, found }— because aletis a statement and needs its semicolon before the brace. - Writing
returnin the tail. Legal, andclippy::needless_returnwill ask you not to. Keepreturnfor genuine early exits. - Reaching for a block to end a borrow that already ended. Non-lexical lifetimes made most of those blocks unnecessary in 2018. Try deleting it; the compiler will tell you if it was load-bearing.
- Reading the
;after}as the block's semicolon. It ends theletstatement, and leaving it out is a syntax error. The semicolon that decides what the block is worth is the one on the line above it. - Reading the nested-block snippet as the shadowing lesson. It is the version every language has. The Rust-specific one is a second
letin the same scope, and it needs no braces — SHADOWING.md is the map. - Expecting
ifwithoutelseto have a value. It has one, and the compiler says which:let x = if c { 5 };iserror[E0317]: \if` may be missing an `else` clause, with the note *"ifexpressions withoutelseevaluate to()`"*. Every path has to produce the type you asked for, and a missing branch is a path.
Practice¶
The semicolon that changed the type. Four parts, and the first is a compile error worth causing on purpose.
Write a mean(scores: &[u32]) -> u32 whose body ends in total / n; — with the semicolon — and read the error before fixing it. Say out loud which line the ^^^ is under and which line actually caused it.
Then take a function that builds a Vec with a mut accumulator and reshape it so that the mut binding never escapes the braces it was needed in.
Then take an if/else if/else chain that assigns into a mut placeholder in every branch, and make the if itself the value. Delete the final else and predict the error before you read it.
Finally, take a borrow that the compiler complains outlives its welcome, wrap it in a block, and then check whether the block was needed at all by deleting it again.
Solution
a_block_is_an_expression_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: the semicolon that changed the type.
//!
//! Four parts, and the first one is a compile error worth causing on purpose.
//! (1) A tail expression grows a semicolon and the function's return type stops
//! matching. (2) A `mut` builder is sealed behind a block expression, so what
//! escapes is an immutable binding. (3) An assign-in-every-branch `mut` becomes
//! an `if` expression, because `if` is built from blocks and therefore has a
//! value. (4) A borrow is given an end by putting it in a block.
//!
//! rustc --edition 2024 a_block_is_an_expression_kata.rs -o /tmp/abiek && /tmp/abiek
fn banner(title: &str) {
println!("\n──── {title}");
}
/// The fixed version. With `;` after `total / n` this is `E0308`.
fn mean(scores: &[u32]) -> u32 {
let total: u32 = scores.iter().sum();
let n = scores.len() as u32;
total / n
}
fn main() {
banner("Part 1: the semicolon that changed the type");
println!(" fn mean(scores: &[u32]) -> u32 {{");
println!(" let total: u32 = scores.iter().sum();");
println!(" let n = scores.len() as u32;");
println!(" total / n; <- one character, and the body is ()");
println!(" }}");
println!();
println!(" error[E0308]: mismatched types");
println!(" | ------ ^^^ expected `u32`, found `()`");
println!(" | |");
println!(" | implicitly returns `()` as its body has no tail");
println!(" | or `return` expression");
println!(" | total / n;");
println!(" | - help: remove this semicolon to return this value");
println!();
println!(" Read the help line: rustc is not asking for a `return`. The body");
println!(" is a block, the block's value is its tail, and a semicolon threw");
println!(" the tail away. Without it:");
println!(" mean(&[5, 3, 4]) = {}", mean(&[5, 3, 4]));
banner("Part 2: the builder that hands out something immutable");
let raw = [("Cara", 5), ("Ada", 4), ("Ben", 2), ("Dev", 4)];
let cutoff = 4;
// Everything mutable happens inside the braces; an immutable Vec comes out.
let through = {
let mut v = Vec::new();
for (name, score) in &raw {
if *score >= cutoff {
v.push(*name);
}
}
v.sort_unstable();
v
};
println!(" cutoff {cutoff} -> {through:?}");
println!(" `mut` lived for six lines inside the block. The binding that");
println!(" escaped is plain, so nothing below can push to it.");
banner("Part 3: the branch that IS the value");
for turnout in [61, 50, 12] {
// Not: let mut verdict = ""; if … { verdict = … } else { … }
let verdict = if turnout >= 50 {
"quorate"
} else if turnout >= 25 {
"advisory only"
} else {
"void"
};
println!(" turnout {turnout:>3}% -> {verdict}");
}
println!(" No `mut`, no placeholder value, and the compiler checks that");
println!(" every arm produced one — a missing `else` would not compile.");
banner("Part 4: the borrow that ends where you say");
let mut ballots = vec![5, 3, 4];
let top = {
let view = &ballots; // the borrow starts here...
*view.iter().max().unwrap()
}; // ...and cannot outlive this brace
ballots.push(9); // so the Vec is writable again immediately
println!(" top before the push: {top}");
println!(" ballots now: {ballots:?}");
println!();
println!(" Since 2018 a borrow usually ends at its last USE, so most code");
println!(" no longer needs this. It is still the tool when the compiler");
println!(" disagrees with you about where the last use was.");
}
Verified output of a_block_is_an_expression_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
──── Part 1: the semicolon that changed the type
fn mean(scores: &[u32]) -> u32 {
let total: u32 = scores.iter().sum();
let n = scores.len() as u32;
total / n; <- one character, and the body is ()
}
error[E0308]: mismatched types
| ------ ^^^ expected `u32`, found `()`
| |
| implicitly returns `()` as its body has no tail
| or `return` expression
| total / n;
| - help: remove this semicolon to return this value
Read the help line: rustc is not asking for a `return`. The body
is a block, the block's value is its tail, and a semicolon threw
the tail away. Without it:
mean(&[5, 3, 4]) = 4
──── Part 2: the builder that hands out something immutable
cutoff 4 -> ["Ada", "Cara", "Dev"]
`mut` lived for six lines inside the block. The binding that
escaped is plain, so nothing below can push to it.
──── Part 3: the branch that IS the value
turnout 61% -> quorate
turnout 50% -> quorate
turnout 12% -> void
No `mut`, no placeholder value, and the compiler checks that
every arm produced one — a missing `else` would not compile.
──── Part 4: the borrow that ends where you say
top before the push: 5
ballots now: [5, 3, 4, 9]
Since 2018 a borrow usually ends at its last USE, so most code
no longer needs this. It is still the tool when the compiler
disagrees with you about where the last use was.
See also¶
- The braces take a name, not an expression — the other thing every line above is quietly using:
{n}in a format string - Scope is about names, not values — the word Job 1 uses, and the two other things it is asked to mean: when a value dies, and when a borrow stops mattering
- What a warning is asking —
unused variableon a block's working name, and what the two answers to it mean - When to shadow — where "give the shadow an end" is the fix
- A shadow does not drop — why a block is the way to free something early, when nothing else will
- Borrowing — the last-use rule, and when a block is still the answer
- Scope is about names, not values — what job 1 actually ends, and the two other things "out of scope" is asked to mean
- Initial values — the other route away from
mut: declare without initializing and let the compiler prove you assigned - SHADOWING.md — the map, if the nested-
nsnippet is what brought you here
Po polsku¶
Klamry robią w Ruście dwie rzeczy naraz i o tym jest cała ta lekcja. Pierwsza jest znajoma z każdego języka: { } otwiera zasięg, a nazwa zadeklarowana w środku przestaje istnieć na zamykającej klamrze — nie jest przesłonięta, tylko jej nie ma (error[E0425]: cannot find value ... in this scope). Druga jest nowa: blok ma wartość, a tą wartością jest jego ostatnia linijka napisana bez średnika. Tour of Rust nazywa to „blokiem wyrażeniowym” i warto zapamiętać ten termin, bo z niego wynika cała reszta składni — ciało funkcji bez return, if po prawej stronie let, match użyty jako wartość.
Polskie kursy programowania wbijają rozróżnienie instrukcja (statement) i wyrażenie (expression) od pierwszych zajęć, a tutaj to rozróżnienie nagle zaczyna pracować: w Ruście prawie wszystko jest wyrażeniem, a średnik jest przełącznikiem, który zamienia wyrażenie w instrukcję. Instrukcja nie ma wartości, więc wartością bloku staje się (), czyli typ jednostkowy (unit type). { tally() } to 6, a { tally(); } to () — ten sam blok, różnica jednego znaku, dwa różne typy. Średnik nie jest tu interpunkcją kończącą linijkę, tylko operatorem, który wyrzuca wartość.
Dlatego najczęstszy błąd pierwszego tygodnia wygląda tak: fn double(n: i32) -> i32 { n * 2; } daje error[E0308]: mismatched types, a daszki ^^^ wskazują typ zwracany, nie średnik. Kompilator mówi „obiecałeś i32, a to ciało produkuje ()”, natomiast przyczyna leży cztery linijki niżej, w podpowiedzi help: remove this semicolon to return this value. Odruch, żeby „naprawić” to zmianą sygnatury, usuwa komunikat i zostawia bezużyteczną funkcję — poprawia się ciało, nie nagłówek. W drugą stronę: return n * 2; w ostatniej linii skompiluje się bez problemu, ale clippy::needless_return (włączony domyślnie) poprosi o skasowanie return. Tutaj return zostaje do wcześniejszego wyjścia ze środka funkcji; na końcu jest szumem — i to jest przyzwyczajenie, które trzeba świadomie oduczyć, przychodząc z C, Javy czy Pythona.
Po co się tego używa w praktyce? Najczęściej po to, żeby zamknąć mut wewnątrz bloku: let ballots = { let mut v = Vec::new(); ... v }; oddaje na zewnątrz zwykłe, niemutowalne wiązanie, więc żadna linijka poniżej nie dopisze już nic do wektora. Dalej — żeby wyznaczyć przesłanianiu (shadowing) wyraźny koniec i żeby zakończyć pożyczenie (borrow), przy czym tu uwaga: od 2018 roku pożyczenie kończy się w miejscu ostatniego użycia, więc większość takich bloków w starszych polskich poradnikach jest już zbędna (skasuj i sprawdź — kompilator powie, czy blok był potrzebny). Z tej samej zasady wynika jeszcze jedno: if użyty jako wartość musi mieć else, bo brakująca gałąź to ścieżka bez wartości — error[E0317], z notką „if expressions without else evaluate to ()”.
Szukaj po polsku: blok wyrażeniowy · wyrażenie a instrukcja · rust tail expression · rust E0308 mismatched types semicolon · rust clippy needless_return · rust E0317 if may be missing an else clause