Skip to content

Rust Scope

Scope is the region of code where a variable is valid. It’s a simple idea, but understanding it makes a lot of Rust’s other rules (especially ownership) click into place.

A variable exists from the point it’s declared until the closing } of the block it’s in:

fn main() {
let x = 5; // x comes into scope here
{
let y = 10; // y comes into scope here
println!("x = {x}, y = {y}"); // both are valid
} // y goes out of scope here
println!("x = {x}");
// println!("{y}"); // ❌ error: y is no longer in scope
}

Output:

x = 5, y = 10
x = 5
▶ Try it Yourself

y only exists inside the inner { } block. Once that block ends, y is gone — trying to use it afterward is a compile error, not a runtime surprise.

Scope only goes one way: an inner block can see the variables of the blocks around it (that’s how the inner block prints x), but an outer block can’t see into an inner one.

The parameters and any variables you create inside a function only exist for the duration of that function call:

fn calculate() -> i32 {
let result = 5 * 2; // result only exists inside calculate
result
}
fn main() {
let value = calculate();
println!("{value}");
// println!("{result}"); // ❌ error: result doesn't exist out here
}

Output:

10
▶ Try it Yourself

The same is true the other way round: a function can’t see the local variables of main. If it needs a value, you pass it in as a parameter, as you saw on the Functions page.

A { } block doesn’t just limit where variables live. It can also produce a value: the last expression inside it, written without a semicolon. That’s the same rule that makes a function return its last line.

This is handy when you need a few helper variables to work something out, but don’t want them hanging around afterwards:

fn main() {
let price = 40;
let total = {
let tax = price / 10; // tax only lives inside this block
let shipping = 5;
price + tax + shipping // no semicolon: this is the block's value
};
println!("total = {total}");
// println!("{tax}"); // ❌ error: tax is out of scope
}

Output:

total = 49
▶ Try it Yourself

Two details to watch:

  • The last line price + tax + shipping has no semicolon. If you add one, the block gives back () (nothing) instead of a number, and println! can’t print it.
  • The whole thing is a let statement, so it ends with }; after the closing brace.

Once the block ends, tax and shipping are gone. Only the result, total, is left. Keeping helpers in a small block like this makes it clear they’re only needed for one calculation.

This connects directly to ownership, which you’ll meet a little later: a value is automatically cleaned up the moment its owner’s scope ends.

fn main() {
{
let text = String::from("hello");
println!("{text}");
} // text's scope ends here — Rust drops it automatically
}

Output:

hello
▶ Try it Yourself

That’s the whole mechanism behind Rust needing no garbage collector: it already knows, at compile time, exactly where every scope ends.

Remember shadowing from the Variables page? Each let with the same name inside a scope creates a distinct variable — it doesn’t erase the outer one, it just takes over for the rest of that scope:

fn main() {
let x = 5;
{
let x = x * 2; // a new x, only inside this block
println!("inner x = {x}"); // 10
}
println!("outer x = {x}"); // 5 — unaffected
}

Output:

inner x = 10
outer x = 5
▶ Try it Yourself

This program doesn’t compile, because area is used after its block has ended:

fn main() {
let width = 3;
{
let height = 4;
let area = width * height;
}
println!("area = {area}");
}

Change it so it prints area = 12, while height still lives only inside the block. Hint: make the block produce the area, and store the result in a variable declared outside it.

Show solution
fn main() {
let width = 3;
let area = {
let height = 4;
width * height
};
println!("area = {area}");
}

Output:

area = 12
▶ Try it Yourself

area now lives in main’s scope, so println! can see it. height is still only needed inside the block, and it’s dropped when the block ends.

Create a variable level set to 1. Open a block that shadows it with 2, and inside that, another block that shadows it with 3. Print level in each block, and again right after each block closes. Hint: you should see the numbers go 1, 2, 3, then back down to 2 and 1.

Show solution
fn main() {
let level = 1;
println!("level {level}");
{
let level = 2;
println!("level {level}");
{
let level = 3;
println!("level {level}");
}
println!("back to level {level}");
}
println!("back to level {level}");
}

Output:

level 1
level 2
level 3
back to level 2
back to level 1
▶ Try it Yourself

Each inner level hides the outer one only until its block ends. The outer variables were never changed, so they’re still there, waiting.

  • Scope is the region (marked by { }) where a variable is valid.
  • A variable stops existing the instant its scope’s closing } is reached.
  • Function bodies are scopes too — locals inside a function disappear when it returns.
  • A block is an expression: its last line (no semicolon) is its value, and its helper variables vanish when it ends.
  • Scope is what tells Rust exactly when to drop a value, which is why no garbage collector is needed.

Quick check

1. Why doesn't Rust need a garbage collector, according to this page?

2. What happens when you shadow a variable with let x = x * 2; inside an inner block?

3. What is the value of total after let total = { let a = 2; a * 10 };?

Score: 0 / 3