Rust Scope

Rust uses lexical, block-based scoping to determine exactly when a variable becomes valid and when it is cleaned up.

What Is a Scope?

A scope is the region of a program where a binding (a variable name) is valid. In Rust, scopes are almost always defined by curly braces `{ }`. A variable becomes valid the moment it is declared with `let`, and it remains valid until the end of the innermost block that contains it. Once the closing brace of that block is reached, the variable goes out of scope and Rust automatically calls `drop` on it if it owns any resources.

Block Expressions

In Rust, a pair of braces `{ }` is itself an expression, called a block expression. Blocks can appear almost anywhere a value is expected: as the body of a function, as an arm of an `if`, or standalone inside another block. The last expression in a block (without a trailing semicolon) becomes the value of the whole block.

A Block as an Expression

fn main() {
    let x = 5;

    let y = {
        let inner = x * 2;
        inner + 1 // no semicolon: this is the block's value
    };

    println!("x = {}, y = {}", x, y);
    // `inner` is not accessible here; it went out of scope
}

Nested Scopes and Shadowing

Scopes can be nested inside one another, and an inner scope can declare a variable with the same name as one in an outer scope. This is called shadowing. The inner variable temporarily hides the outer one for the rest of the inner block, and the outer variable reappears once the inner block ends. Shadowing is different from mutation: each `let` creates a brand-new binding, potentially with a different type.

Nested Scope with Shadowing

fn main() {
    let count = 1;
    println!("outer count: {}", count);

    {
        let count = count + 10; // shadows the outer `count`
        println!("inner count: {}", count);

        {
            let count = "now a string"; // shadowing can change type
            println!("innermost count: {}", count);
        }

        println!("back to inner count: {}", count);
    }

    println!("back to outer count: {}", count);
}

Scope and the Drop Order

When multiple variables are declared in the same block, Rust drops them in reverse order of declaration once the block ends. This matters when values reference or depend on each other, and it is one of the mechanical rules that makes Rust's ownership system predictable rather than magical.

  • A variable's scope begins at its `let` declaration, not at the top of the block.
  • A variable's scope ends at the closing brace `}` of the innermost enclosing block.
  • Function parameters are scoped to the entire function body.
  • Values are dropped in the reverse order they were created within a scope.
  • Shadowed variables are entirely separate bindings; the old one is simply inaccessible, not destroyed early.

Drop Order Demonstration

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

fn main() {
    let _a = Noisy("a");
    let _b = Noisy("b");
    let _c = Noisy("c");
    println!("end of main reached");
    // prints: dropping c, dropping b, dropping a
}
Note: Use small, deliberately-scoped blocks to force early cleanup of a value, such as a file handle or a lock guard, well before the end of a long function.

Scope vs. Lifetime

Scope is a purely lexical concept: it is about where in the source code a name is visible. Lifetime, which you will meet properly when studying borrowing, is about how long a reference remains valid. The two are related — a reference's lifetime can never outlive the scope of the value it points to — but they are not identical, and conflating them is a common source of confusion for newcomers.

ConceptQuestion it answersDetermined by
ScopeWhere is this name visible?Position of braces in source code
LifetimeHow long is this reference valid?How long the borrowed data actually lives
OwnershipWho is responsible for cleanup?Which binding currently owns the value
Note: The Rust compiler's borrow checker uses scope information as one of the primary inputs for computing lifetimes, which is why understanding scope thoroughly makes the borrow checker feel far less mysterious.

Exercise: Rust Scope

When does a variable declared inside a `{ }` block go out of scope?