Blocks and Scope
A block is a sequence of statements and an optional final expression enclosed in curly braces { }. A block is also an expression, so it evaluates to a value. Scope is the region of code in which a name is visible. In Rust, every block creates a new scope.
fn main() {
let outer = "outer";
{
let inner = "inner";
println!("{outer}, {inner}");
}
println!("{outer}");
}
outer, inner
outer
The inner block can see outer because it is nested inside the scope where outer was declared. After the closing brace, inner no longer exists.
Block Syntax
{
statement;
statement;
expression <- optional, no semicolon: the value of the block
}
A block can contain:
letstatements- Expression statements
- Items such as functions, constants, structs, and
usedeclarations - Nested blocks
- One optional tail expression at the end
Blocks appear in many places. A function body is a block. The bodies of if, else, match arms, loop, while, for, and closures are blocks. A bare { ... } is also valid on its own.
fn main() {
for i in 0..3 {
let squared = i * i;
println!("{i} squared is {squared}");
}
let mut n = 0;
while n < 2 {
let message = format!("n is {n}");
println!("{message}");
n += 1;
}
if n == 2 {
let note = "n reached two";
println!("{note}");
}
let kind = match n {
2 => {
let text = "two";
text
}
_ => "other",
};
println!("{kind}");
}
0 squared is 0
1 squared is 1
2 squared is 4
n is 0
n is 1
n reached two
two
Each of these blocks has its own scope. Variables such as squared, message, and note exist only inside the block that declares them.
Blocks Are Expressions
The value of a block is the value of its tail expression, the last expression written without a semicolon. A block without a tail expression evaluates to ().
fn main() {
let x = 5;
let y = {
let x = x + 1;
x * 2
};
println!("x = {x}, y = {y}");
}
x = 5, y = 12
Inside the block, let x = x + 1 creates a new x that hides the outer one. The tail expression x * 2 uses the inner x, which is 6, so y is 12. After the block ends, x refers to the outer variable again.
Adding a semicolon to the last line removes the tail expression and changes the type of the block:
fn main() {
let x = {
let y = 3;
y + 1;
};
println!("{x:?}");
}
warning: unused arithmetic operation that must be used
--> g7.rs:4:9
|
4 | y + 1;
| ^^^^^ the arithmetic operation produces a value
|
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = y + 1;
| +++++++
warning: 1 warning emitted
()
Here x is (). Using it with {} instead of {:?} is an error, because () has no Display implementation:
// ❌ does not compile
fn main() {
let x = {
let y = 3;
y + 1;
};
println!("{x}");
}
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> g6.rs:7:15
|
7 | println!("{x}");
| ^^^ `()` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `()`
= note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
Blocks Anywhere a Value Is Expected
Because a block is an expression, it can be used as a function argument, in an arithmetic expression, or on the right side of any let.
fn main() {
let total = add({ 1 + 1 }, {
let k = 3;
k * 2
});
let label = format!("{}", {
let a = 1;
a + 1
});
println!("{total} {label}");
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
8 2
A block around a single expression, such as { 1 + 1 }, adds nothing, and the compiler warns about it with unused_braces. Blocks are useful when a value needs local helper variables.
Limiting Mutability to a Block
A block can build a value using mutable variables and then hand it out as an immutable binding:
fn main() {
let numbers = {
let mut list = Vec::new();
list.push(1);
list.push(2);
list.push(3);
list
};
// `numbers` is immutable from here on.
println!("{numbers:?}");
}
[1, 2, 3]
list is mutable only inside the block. The outer numbers is declared without mut.
Labeled Blocks
A block can be given a label, and break 'label value exits the block early with a value. This avoids a nested if chain when a search has several exit points.
fn main() {
let values = [3, 8, 12, 5];
let found = 'search: {
for value in values {
if value > 10 {
break 'search Some(value);
}
}
None
};
println!("{found:?}");
}
Some(12)
The break leaves the labeled block, not only the for loop. If the loop finishes without finding a value, execution reaches the tail expression None.
Special Blocks
A block can carry a keyword in front of it that changes how it is treated:
| Form | Meaning |
|---|---|
unsafe { ... } |
Allows operations the compiler cannot verify, such as dereferencing a raw pointer |
const { ... } |
The block is evaluated at compile time |
async { ... } |
Produces a future instead of running immediately |
'label: { ... } |
A block that break 'label can exit |
fn main() {
let number = 5;
let pointer = &number as *const i32;
let copy = unsafe { *pointer };
let constant = const { 2 * 8 };
println!("{copy} {constant}");
}
5 16
Scope Rules
A name is visible from the point where it is declared to the closing brace of the block that contains the declaration. The scope includes every block nested inside it.
fn main() {
let a = 1; <- a begins
{
let b = a + 1; <- b begins; a is visible
{
let c = b + 1; <- c begins; a and b are visible
println!("{a} {b} {c}");
} <- c ends
} <- b ends
} <- a ends
fn main() {
let a = 1;
{
let b = a + 1;
{
let c = b + 1;
println!("{a} {b} {c}");
}
}
}
1 2 3
Code in an inner block can read and modify variables from an outer block, provided the variable is declared mut for modification:
fn main() {
let mut count = 0;
{
count += 1;
let step = 5;
count += step;
}
println!("count = {count}");
}
count = 6
Names Do Not Escape Their Block
Using a name after its block has ended is an error:
// ❌ does not compile
fn main() {
{
let inner = 10;
}
println!("{inner}");
}
error[E0425]: cannot find value `inner` in this scope
--> d2.rs:6:16
|
6 | println!("{inner}");
| ^^^^^
|
help: the binding `inner` is available in a different scope in the same function
--> d2.rs:4:13
|
4 | let inner = 10;
| ^^^^^
The same applies to variables declared inside for, while, if, and match blocks:
// ❌ does not compile
fn main() {
for i in 0..3 {
let squared = i * i;
}
println!("{i} {squared}");
}
error[E0425]: cannot find value `i` in this scope
--> h6.rs:6:16
|
6 | println!("{i} {squared}");
| ^ not found in this scope
error[E0425]: cannot find value `squared` in this scope
--> h6.rs:6:20
|
6 | println!("{i} {squared}");
| ^^^^^^^
|
help: the binding `squared` is available in a different scope in the same function
--> h6.rs:4:13
|
4 | let squared = i * i;
| ^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0425`.
The loop variable i belongs to the loop's scope too.
Names Cannot Be Used Before Declaration
Unlike items, a let binding is visible only after its declaration:
// ❌ does not compile
fn main() {
println!("{later}");
let later = 1;
}
error[E0425]: cannot find value `later` in this scope
--> g4.rs:3:16
|
3 | println!("{later}");
| ^^^^^ not found in this scope
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0425`.
Shadowing in Inner Blocks
Declaring a variable with the same name as an outer variable shadows it. The inner name hides the outer one until the inner block ends, and then the outer one is visible again.
fn main() {
let x = 1;
println!("outer x = {x}");
{
let x = 2;
println!("inner x = {x}");
{
let x = x * 10;
println!("innermost x = {x}");
}
println!("inner x again = {x}");
}
println!("outer x again = {x}");
}
outer x = 1
inner x = 2
innermost x = 20
inner x again = 2
outer x again = 1
Shadowing creates a new variable. It does not change the old one. Shadowing in the same scope, and how it differs from mut, is covered with variables.
Items Inside Blocks
Items such as functions and constants can be declared inside a block. They follow the same scope rule: they are visible only within that block. Unlike let bindings, items can be used anywhere in the block, including before their declaration.
fn main() {
let value = 10;
let result = {
fn helper(n: i32) -> i32 {
n * 3
}
const OFFSET: i32 = 4;
helper(value) + OFFSET
};
println!("{result}");
}
34
An item inside a block cannot use the block's local variables. helper receives value as an argument because the function body cannot capture it.
Using the item outside its block is an error:
// ❌ does not compile
fn main() {
{
fn helper() -> i32 {
1
}
}
println!("{}", helper());
}
error[E0425]: cannot find function `helper` in this scope
--> d8.rs:8:20
|
8 | println!("{}", helper());
| ^^^^^^ not found in this scope
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0425`.
Scope and Ownership
Scope also decides when values are cleaned up. When a block ends, every variable declared in it is dropped: the value's destructor runs and its resources, such as heap memory, are released. Variables are dropped in the reverse order of their declaration.
The following example uses a type that prints a message when it is dropped. The Drop trait is covered later; here it only makes the timing visible.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let _outer = Noisy("outer");
{
let _first = Noisy("first");
let _second = Noisy("second");
println!("end of inner block");
}
println!("after inner block");
}
end of inner block
drop second
drop first
after inner block
drop outer
second is dropped before first, and both are dropped before the code after the inner block runs. outer is dropped when main ends.
A shadowed variable is not dropped when it is shadowed. It lives until the end of its scope, even though its name is no longer reachable:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let _value = Noisy("first");
let _value = Noisy("second");
println!("end of main");
}
end of main
drop second
drop first
Moving a Value Out of a Block
A value moved into an inner block is owned by that block's variable and is dropped at the end of the block, so the original name can no longer be used:
// ❌ does not compile
fn main() {
let message = String::from("hello");
{
let moved = message;
println!("{moved}");
}
println!("{message}");
}
error[E0382]: borrow of moved value: `message`
--> g10.rs:8:16
|
3 | let message = String::from("hello");
| ------- move occurs because `message` has type `String`, which does not implement the `Copy` trait
4 | {
5 | let moved = message;
| ------- value moved here
...
8 | println!("{message}");
| ^^^^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
5 | let moved = message.clone();
| ++++++++
References Cannot Outlive Their Scope
A reference to a variable cannot be used after the variable's block has ended, because the variable has been dropped. The compiler rejects this:
// ❌ does not compile
fn main() {
let reference;
{
let value = 42;
reference = &value;
}
println!("{reference}");
}
error[E0597]: `value` does not live long enough
--> d5.rs:6:21
|
5 | let value = 42;
| ----- binding `value` declared here
6 | reference = &value;
| ^^^^^^ borrowed value does not live long enough
7 | }
| - `value` dropped here while still borrowed
8 | println!("{reference}");
| --------- borrow later used here
This check is how Rust prevents dangling references.
Temporaries at the End of a Block (Edition 2024)
A value that an expression creates temporarily, such as the guard returned by cell.borrow(), is dropped at the end of a statement. For a tail expression, the point at which temporaries are dropped changed in Rust 2024. In 2024, temporaries in the tail expression are dropped before the block's local variables. In 2021 and earlier, they were dropped after.
use std::cell::RefCell;
fn length() -> usize {
let cell = RefCell::new("hello");
cell.borrow().len()
}
fn main() {
println!("{}", length());
}
5
This compiles in edition 2024. Compiled with --edition 2021, the temporary Ref guard returned by borrow() outlives cell and the compiler reports:
error[E0597]: `cell` does not live long enough
--> h1.rs:5:5
|
4 | let cell = RefCell::new("hello");
| ---- binding `cell` declared here
5 | cell.borrow().len()
| ^^^^---------
| |
| borrowed value does not live long enough
| a temporary with access to the borrow is created here ...
6 | }
| -
| |
| `cell` dropped here while still borrowed
| ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `Ref<'_, &str>`
|
= note: the temporary is part of an expression at the end of a block;
consider forcing this temporary to be dropped sooner, before the block's local variables are dropped
help: for example, you could save the expression's value in a new local variable `x` and then make `x` be the expression at the end of the block
|
5 | let x = cell.borrow().len(); x
| +++++++ +++