Scope & Lifetime

Scope answers 'where in the code can this name be seen?' Lifetime answers 'how long does this piece of storage actually exist in memory?' The two are related but genuinely distinct, and confusing them is a common source of subtle bugs.

Scope is the region of code where a variable's name is visible and can be referenced. Lifetime is how long a variable's actual storage exists before it's destroyed or reclaimed. A local variable declared inside a function typically has both a scope limited to that function and a lifetime limited to that single call.

Limiting scope prevents unrelated parts of a program from accidentally reading or modifying each other's data, and helps keep large programs understandable by making clear exactly where each name matters. Understanding lifetime prevents using data that no longer exists, or unnecessarily keeping data around after it's no longer needed.

A loop counter that only makes sense inside its loop, a temporary calculation result inside a function, a global configuration value meant to be visible throughout an entire program — each is a deliberate choice about the right scope for that particular piece of data.

Scope is like which rooms in a house a particular conversation can be overheard in — a whispered conversation in a closed bedroom (local scope) isn't heard in the kitchen, while an announcement over a house-wide intercom (global scope) is heard everywhere. Lifetime is separately about how long that conversation actually continues before it ends, regardless of which rooms could have heard it.

message is a local variable, scoped entirely to the greet function. Once greet() returns, both its scope and its storage no longer exist — trying to use message outside the function fails.

Common scope levels include: block scope (visible only within an enclosing {} block, common in C-family languages with let/const-style declarations), function scope (visible throughout an entire function, regardless of inner blocks — how JavaScript's older var keyword behaves), and global/module scope (visible throughout an entire file or program).

Lexical (static) scoping — used by most modern languages — determines a name's scope based on where it's textually written in the source code, not based on the runtime call sequence. This is what makes closures possible: an inner function can see variables from its enclosing function's scope simply because of where it was written, regardless of when or how it's eventually called.

A local variable's lifetime is typically tied to its stack frame — created when its function is called, destroyed when that function returns (covered in full in the future Memory & Program Execution module). A variable stored on the heap can have a lifetime extending well beyond the function that created it, managed either manually or by a garbage collector depending on the language.

A closure captures variables from an enclosing scope by reference (in most languages) so it can continue to access and even modify them after the outer function has returned — extending that specific variable's effective lifetime beyond what its scope alone would suggest.

Relying on a variable declared with a function-scoped keyword (like JavaScript's var) expecting it to be limited to an inner block, and being surprised it 'leaks' out to the whole function.

Creating closures inside a loop that all accidentally capture the same shared loop variable, rather than an independent value per iteration — a very common historical JavaScript gotcha, largely resolved in modern JavaScript by using let instead of var, since let creates a new binding per iteration.

Using overly broad (global) scope for values that are only ever needed locally, making the codebase harder to reason about and increasing the risk of unrelated code accidentally interfering with that value.

Confusing 'the variable name still compiles/parses here' with 'the variable's data is still valid here' — a dangling reference to memory whose lifetime has ended is a distinct, more dangerous problem than a simple scope error, covered further in the Pointers & References section.

Declaring loop variables and temporary calculation results with the narrowest scope that still gets the job done, to avoid accidental reuse or interference elsewhere in a large function.

Deliberately using a closure to create a private, encapsulated piece of state — for example, a counter function that increments an internal variable no outside code can access or modify directly except through the function itself.

Preferring block-scoped declarations (let/const in JavaScript, block-scoped variables in most modern languages) over older function-scoped equivalents, specifically to avoid the classic scope-leakage bugs described above.

Stack frames and scope in memory Each function call typically creates a new stack frame holding that call's local variables; when the function returns, the frame is popped and those variables' storage is reclaimed (or becomes invalid to access). This is exactly why a local variable's scope and lifetime usually coincide — but a closure or a returned pointer/reference to a local variable can break that coincidence, which is precisely the source of dangling-reference and lifetime bugs, covered fully in the future Memory & Program Execution module.

Lexical vs. dynamic scoping Lexical (static) scoping resolves a name based on the source code's textual nesting, decided once and for all at compile/parse time. Dynamic scoping — rare in modern mainstream languages but present in some (like older Lisp dialects, and certain shell/Bash variable behaviors) — resolves a name based on the actual call stack at runtime, meaning the same piece of code can resolve a free variable differently depending on who called it and from where. Lexical scoping is overwhelmingly preferred in modern language design because it makes code far easier to reason about statically.

Rust's lifetimes as an explicit, checked concept Rust makes 'lifetime' an explicit part of its type system (visible as annotations like 'a in function signatures involving references) precisely so its compiler can verify at compile time that no reference ever outlives the data it points to — turning what is, in most languages, an informal reasoning burden on the programmer into a formally checked compiler guarantee, a major pillar of Rust's memory-safety-without-garbage-collection story described in the Memory Safety & Determinism section.

What's the difference between scope and lifetime?

Scope is about where in the source code a name is visible and can be referenced. Lifetime is about how long the actual underlying storage for a value exists in memory. They often coincide for simple local variables but can diverge — for example, a closure can keep a variable's storage alive well past the point where its originally enclosing function has returned.

What is a closure?

A function bundled together with references to variables from the scope it was defined in, allowing it to continue accessing (and sometimes modifying) those variables even after the outer function that created them has finished executing.

Output-based: in a naive loop-and-closure setup using var in older JavaScript, why do all created closures end up referencing the same final loop value instead of each their own?

Because var is function-scoped, not block-scoped, so there is only a single shared variable across all loop iterations. Every closure created in the loop captures a reference to that same single variable, which by the time the closures are actually called, holds its final value from the loop's last iteration. Using let instead creates a fresh binding per iteration, avoiding this.

What's the difference between lexical and dynamic scoping?

Lexical scoping resolves a variable's scope based on where it's written in the source code, fixed at compile/parse time. Dynamic scoping resolves it based on the actual sequence of function calls at runtime, so the same code could resolve a name differently depending on who called it.

def greet():
    message = "Hello!"   # local scope: only visible inside greet()
    print(message)

greet()
print(message)   # NameError: 'message' is not defined here