awesome-everything RU
↑ Back to the climb

Base CS from zero

Scope

Crux Scope is which names are visible at a given point in the code. Locals live in their frame and vanish when it pops. The lifetime of a name is tied directly to the lifetime of its frame.
◷ 18 min

You know that each function call gets its own stack frame, and that the frame is popped when the function returns. But what does that mean for the names you use in code? Can one function see the variables of another? Can you use a name before it is declared? What happens to a variable after its function returns?

All of these questions are answered by a single concept: scope. Scope is the rule that says which names are visible at a given point in the code. In most languages, including TypeScript, scope is tied directly to the structure of functions. A variable declared inside a function is visible only inside that function — and it ceases to exist when the function’s frame is popped.

Goal

After this lesson you can define scope as the region of code where a name is visible, explain why locals are invisible outside their function, describe the lifetime of a variable as tied to its frame’s lifetime, and explain what happens to locals when a function returns.

1

Scope: the region where a name is visible. Every name (variable, parameter, function name) is visible in some part of the code and invisible in the rest. The region where a name can be used is called its scope. Outside the scope, the name does not exist from the compiler’s point of view — referencing it is an error.

In TypeScript (and most C-family languages), scope is determined by curly braces { }. A name declared inside a block { ... } is scoped to that block: it is visible from the point of declaration to the closing }, and invisible outside. A function body is a block, so a variable declared inside a function is scoped to that function.

2

Local scope: variables declared inside a function. A variable declared inside a function body is called a local variable. Its scope is the function body — from its declaration to the end of the function. No code outside the function can see this variable by name.

Consider two functions f and g, each declaring a variable named x:

function f() { let x = 1; }
function g() { let x = 2; }

These are two completely different variables, each scoped to their own function. Naming them both x does not cause a conflict: each x refers to a separate cell in a separate stack frame. When f is running, its x is cell in f’s frame; when g is running, its x is a different cell in g’s frame.

3

Lifetime: how long a variable exists. Scope is about where in the code a name is visible. Lifetime is about how long in time the variable exists in memory. For local variables, lifetime is tied directly to the frame:

  • The variable comes into existence when the frame is pushed (the function is called).
  • The variable is destroyed when the frame is popped (the function returns).

There is no way to hold a reference to a local variable after the function returns: the frame cells are immediately reused for the next call. Attempting to use such a reference would read from cells that now belong to someone else — a class of bug called “use-after- free” or “dangling reference”.

Why this works

Why tie variable lifetime to the frame rather than letting variables persist indefinitely? Because the stack’s key advantage is its zero-cost automatic management: pushing and popping a frame is a single arithmetic operation on the stack pointer. If locals could outlive their frames, the runtime would need to track them individually — essentially reinventing the heap. The rule “lifetime = frame lifetime” is what makes local variable allocation so cheap. When you need a value to outlive its creator function, you explicitly put it on the heap (via objects, arrays, or allocation) — and pay the higher cost of heap management.

4

Why g cannot see f’s locals. When g is executing, f’s frame may not even be on the stack (if g was called independently, not by f). Even if f called g and f’s frame is still below g’s on the stack, the scope rule prevents g from accessing f’s locals by name: the compiler does not allow it. This is not just a compiler restriction — it reflects the memory model. f’s locals are cells in f’s frame, which exists at a specific range of addresses in the stack region. g has no named way to reach those addresses.

This isolation is a feature, not a limitation. It means you can write g without worrying about what names f happens to have declared. Each function is a closed, self-contained workspace.

main a=1
frame₀
f x=10
frame₁
g x=20
frame₂
Three frames on the stack. Each function has its own x. g cannot name f's x — they are different cells at different addresses, and scope rules prevent cross-frame name access.
Worked example

Tracing scope through a call sequence.

function add(a: number, b: number): number {
  let sum = a + b;  // sum is local to add, scope = add's body
  return sum;
}

function main(): void {
  let result = add(3, 4);  // result is local to main
  // sum does not exist here — it is out of scope
}

While add is executing:

  • a, b, sum are in scope — they are cells in add’s frame.
  • result is not in scope here (it is in main’s frame, and scope rules prevent cross- frame access by name).

After add returns:

  • add’s frame is popped. a, b, and sum no longer exist.
  • Back in main: result is in scope (it is in main’s frame). The name sum is out of scope — referencing it would be a compile error.

The compiler enforces scope at compile time: if you write sum in main’s body, TypeScript reports an error — “Cannot find name ‘sum’.” The cells that held sum are still physically in memory until reused by the next call, but the name no longer refers to anything valid.

Common mistake

A common mistake is thinking that two functions sharing a variable name means they share the same memory cell. They do not. Each function call creates its own frame; each frame has its own named cells. let x = 1 in function f and let x = 2 in function g create two different cells, each named x within their respective scopes. Changing x in f has no effect on x in g — they are at different addresses.

Practice 0 / 5

function f() { let x = 5; } function g() { /* can g use x here? type 1 for yes, 0 for no */ }

function f() { let x = 5; return x; } — After f returns, does x still exist in memory as a live, named cell? Type 1 for yes, 0 for no.

function f() { let x = 1; } function g() { let x = 2; } — Both f and g declare a variable named x. How many distinct memory cells named x are there when both functions are active?

A function is called 3 times in sequence (not nested). Each call declares a local variable y. How many times is y created and destroyed?

f calls g. While g is executing, f's frame is still on the stack. Can g access f's local variable q by name? Type 1 for yes, 0 for no.

Check yourself
Quiz

What happens to a function's local variables when the function returns?

Recap

Scope is the region of code where a name is visible. In TypeScript, a variable declared inside a function body has local scope: it is visible only within that function, from its declaration to the closing brace. The lifetime of a local variable is tied to its frame’s lifetime — it is created when the frame is pushed (the function is called) and destroyed when the frame is popped (the function returns). Two functions can declare variables with the same name without conflict because each variable lives in its own frame at its own address. No function can access another function’s locals by name — scope rules enforced at compile time prevent it, reflecting the underlying memory model where each frame is a self-contained block of cells.

Continue the climb ↑Recursion preview
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.