Block Expressions
A block expression is a sequence of zero or more statements and an optional final tail expression enclosed in curly braces { ... }. In Rust, any block is an expression that creates a new lexical scope, executes its statements in order, and evaluates to a value.
Anatomy of a Block Expression
let hypotenuse_sq: i32 = {
let dx = 3; <-- 1. Statement (`let` declaration, ends with `;`)
let dy = 4; <-- 2. Statement (`let` declaration, ends with `;`)
println!("computing");<-- 3. Statement (expression statement, ends with `;`)
dx * dx + dy * dy <-- 4. Tail expression (no `;` -> becomes the block's value: `25`)
};
- If a block ends with a tail expression (no trailing
;), the block's type and value are the type and value of that tail expression. - If a block is empty
{}or ends with a semicolon-terminated statement, the block evaluates to the unit value()of type().
fn main() {
let area: u32 = {
let width = 12;
let height = 5;
width * height // Tail expression -> block evaluates to `60`
};
let unit_result: () = {
let temp = 42;
println!("Side-effect block with temp = {temp}"); // Ends with `;` -> evaluates to `()`
};
println!("area = {area}, unit_result = {unit_result:?}");
}
Side-effect block with temp = 42
area = 60, unit_result = ()
Isolating Intermediate Variables with Initialization Blocks
A common Rust idiom uses a block expression on the right-hand side of let to compute a complex value using temporary helper variables without polluting the surrounding function's scope or keeping mut active longer than necessary:
fn main() {
// `raw`, `trimmed`, and `mut buffer` exist ONLY inside the `{ ... }` initialization block!
let formatted_tag: String = {
let raw = " rust-2024 ";
let trimmed = raw.trim();
let mut buffer = String::from("[");
buffer.push_str(&trimmed.to_uppercase());
buffer.push(']');
buffer // Moved out into `formatted_tag` as an IMMUTABLE String!
};
// Here, `raw`, `trimmed`, and `buffer` are out of scope, and `formatted_tag` is immutable:
println!("Result: {formatted_tag}");
}
Result: [RUST-2024]
Shortening Borrow Lifetimes (Scoping Locks and Mutable Borrows)
Because every variable declared inside a block is dropped at the block's closing brace }, standalone blocks { ... } are also used to explicitly end a mutable borrow (&mut T) or release a lock guard (MutexGuard) before the rest of the function continues:
fn main() {
let mut data = vec![10, 20, 30];
{
// `slice_mut` borrows `data` mutably only inside this inner block:
let slice_mut: &mut [i32] = &mut data[0..2];
slice_mut[0] += 5;
slice_mut[1] += 5;
} // `slice_mut` goes out of scope here!
// `data` can now be borrowed immutably or mutated freely:
data.push(40);
println!("data = {data:?}");
}
data = [15, 25, 30, 40]
Items Declared Inside Block Expressions
In addition to let statements and expression statements, a block expression can contain local item declarations—including fn, struct, enum, const, type, and use statements! Items declared inside a block are scoped strictly to that block:
fn main() {
let scaled = {
const FACTOR: i32 = 10;
struct Pair(i32, i32);
fn sum_pair(p: Pair) -> i32 {
(p.0 + p.1) * FACTOR
}
sum_pair(Pair(3, 4))
};
println!("scaled = {scaled}");
}
scaled = 70
Early Exit from a Block Expression ('label: { ... })
By attaching a label 'label: to a block expression, you can exit the block early from an inner if or match using break 'label value; without returning from the enclosing function:
fn main() {
let override_val: Option<i32> = None;
let result: i32 = 'compute: {
if let Some(v) = override_val {
break 'compute v; // Exits `'compute` block immediately with `v`
}
let base = 20;
base * 2 + 2 // Normal tail expression of `'compute`
};
println!("result = {result}");
}
result = 42
Block Expressions as Function Arguments and Arithmetic Operands
Because a { ... } block is a first-class expression, it can appear anywhere a value expression is expected—including inside function arguments or arithmetic expressions:
fn main() {
let total = 100 + {
let bonus = 25;
bonus * 2
};
println!("total = {total}");
}
total = 150