Functions

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 let bindings are strictly sequential: A variable is only in scope after its let declaration 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 with error[E0415]: identifier 'x' is bound more than once in this parameter list).
  • However, inside the function body, a let statement 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:

  1. Local variables declared inside the function body are dropped in reverse declaration order (last declared drops first).
  2. 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