Function Scope
Every Rust function defines an isolated lexical scope for local variables and parameters. Unlike nested blocks { ... } inside a function—which can freely access variables from enclosing blocks—a function body cannot capture or access local variables from its caller or from an enclosing function unless they are passed explicitly as parameters.
Isolation of Local Variables Across Functions
Parameters and let bindings declared inside a function exist only during that function's execution and are completely invisible to any other function:
+---------------------------------------------------+
| fn main() { |
| let api_key = "secret"; (Local to `main`) |
| connect(); -------------------+ |
| } | |
+-------------------------------------------------|-+
|
+-------------------------------------------------v-+
| fn connect() { |
| // `api_key` is NOT in scope here! |
| // Only parameters of `connect`, its own |
| // `let` bindings, and module items are |
| // visible inside `connect`. |
| } |
+---------------------------------------------------+
Attempting to read a caller's local variable inside a called function fails with E0425:
// ❌ does not compile
fn print_count() {
println!("Count is {count}");
}
fn main() {
let count = 42;
print_count();
}
error[E0425]: cannot find value `count` in this scope
--> src/main.rs:2:25
|
2 | println!("Count is {count}");
| ^^^^^ not found in this scope
error: aborting due to 1 previous error
To give print_count access to count, pass it as a parameter (fn print_count(count: i32)).
What Is Visible Inside a Function Scope
Although a function cannot see local variables from other functions, it can access four categories of identifiers:
| Category | Example | Visibility Rule |
|---|---|---|
| Function parameters | fn add(x: i32, y: i32) |
In scope throughout the entire function body |
Local let bindings |
let sum = x + y; |
In scope from the line after the let statement until the end of their enclosing block |
| Module-level items | const, static, fn, struct, enum, type, use |
Visible everywhere in the module regardless of declaration order |
| Prelude items | Option, Some, None, Result, Vec, String, println! |
Automatically imported into every module scope by the standard library prelude |
const TAX_RATE: f64 = 0.08; // Module-level constant: visible inside all functions
fn total_with_tax(subtotal: f64) -> f64 {
let tax = subtotal * TAX_RATE;
subtotal + tax
}
fn main() {
let price = 50.0;
println!("Total: ${:.2}", total_with_tax(price));
}
Total: $54.00
Declaration Order of Functions vs Local Variables
Within a module or block, items (such as fn, const, struct, and enum) and local variable bindings (let) obey two different scoping rules:
- Functions and items are order-independent: A function can call another function defined later in the same file, and two functions can call each other (mutual recursion) without forward declarations.
- Local
letbindings are strictly sequential: A variable is only in scope after itsletdeclaration statement.
fn main() {
// Calling `first_step` and `is_even` before their definitions in the file:
first_step();
println!("4 is even: {}", is_even(4));
}
fn first_step() {
second_step();
}
fn second_step() {
println!("Executed second_step (defined below first_step)");
}
fn is_even(n: u32) -> bool {
if n == 0 { true } else { is_odd(n - 1) }
}
fn is_odd(n: u32) -> bool {
if n == 0 { false } else { is_even(n - 1) }
}
Executed second_step (defined below first_step)
4 is even: true
Parameter Scope and Shadowing Inside a Function
Function parameters enter scope at the opening brace { of the function body and remain in scope until the closing brace }.
- Two parameters in the same function signature cannot have the same name (
fn bad(x: i32, x: i32)fails witherror[E0415]: identifier 'x' is bound more than once in this parameter list). - However, inside the function body, a
letstatement can shadow a parameter name, creating a new local variable that hides the original parameter for the rest of the function:
fn sanitize_port(port: i32) -> u16 {
// Shadow the `port: i32` parameter with a clamped `u16` local binding
let port: u16 = port.clamp(1024, 65535) as u16;
port
}
fn main() {
println!("Sanitized(80) = {}", sanitize_port(80));
println!("Sanitized(8080) = {}", sanitize_port(8080));
println!("Sanitized(99999) = {}", sanitize_port(99999));
}
Sanitized(80) = 1024
Sanitized(8080) = 8080
Sanitized(99999) = 65535
Cleanup at Function Exit
When a function finishes executing, every local variable and owned parameter still held by that function goes out of scope and is dropped before control returns to the caller:
- Local variables declared inside the function body are dropped in reverse declaration order (last declared drops first).
- Function parameters passed by value (and not moved into the return value or another consumer) are dropped after the function's local variables.
struct Tracked(&'static str);
impl Drop for Tracked {
fn drop(&mut self) {
println!(" [Drop] {}", self.0);
}
}
fn worker(_param_a: Tracked, _param_b: Tracked) {
let _local_1 = Tracked("local_1");
let _local_2 = Tracked("local_2");
println!("Inside worker body");
}
fn main() {
worker(Tracked("param_a"), Tracked("param_b"));
println!("Back in main");
}
Inside worker body
[Drop] local_2
[Drop] local_1
[Drop] param_b
[Drop] param_a
Back in main