Skip to main content
CodeFlow
functions~14 minRoute 01 8 of 12

Scope & Closures

Where a name lives — and how a function can carry its surroundings with it.

Prerequisites:Functions

Story — nested rooms with name tags

Picture a building with nested rooms. The innermost room has its own name tags. The room around it has more. The outermost room — the global lobby — has the rest. When code asks "where is x?", it looks in the current room first. Not here? Step outward. Still not here? Step out again. The first x it finds wins.

That nested-search rule is lexical scope. And here's the magic: when a function is born in an inner room, it doesn't forget that room — even after the function is carried out into the lobby and called there. The room's names travel with the function. That packaging is a closure.

Scope says

A counter that remembers between calls without a class. Event handlers that know which button they belong to. Curried functions. All the same trick: inner function, captured outer variables.

See it — the inner function carries its world

makeCounter defines count in its local scope and returns an inner function. makeCounter finishes — and yet, every time you call the inner function, it can still read and update count. That variable is alive only because something outside is holding the inner function.

Scope says

A name search starts in the innermost scope and walks outward. The first match wins. Closures capture variables from the scope they were born in — even after that scope has returned.

Try it — name a closure use case

Exercise. Name a real-world programming task where a closure is the simplest tool — and rewriting it without one would require a class or a global.

Reveal answer

Examples: a debounce wrapper that stores the most recent timer between calls; once(fn) that runs fn the first time and ignores subsequent calls; a click handler factory that takes a button name and returns a function that logs that name. All three are one-liners with closures and several lines without.

Code it — five languages, one captured variable

Python needs nonlocal to write to an outer variable (reads work without it). JavaScript and Go need no annotation at all. Java needs the variable to be effectively final, so we use an AtomicInteger. C++ captures must be explicit: [count] by value, [&count] by reference.

def make_counter():
    count = 0                    # captured by inner
    def inc():
        nonlocal count           # without this, count is read-only
        count += 1
        return count
    return inc

c = make_counter()
print(c(), c(), c())             # 1 2 3
print(c())                       # 4 — count survived after make_counter returned

Quiz it — make it stick

  1. Question 1

    What's the rule for resolving a name reference in a function?

  2. True or false — Question 2

    A closure can read variables from the scope it was born in, even after that scope has exited.

  3. Predict — Question 3

    What does this JavaScript print?

    const fs = [];
    for (var i = 0; i < 3; i++) {
      fs.push(() => i);
    }
    console.log(fs.map(f => f()));

No Dumb Questions

Real questions other learners asked on this page.

  • What is "lexical" scope?
    Lexical means "from the source code" — a name resolves based on where the function was written, not where it was called. Almost every modern language is lexically scoped. (Some early languages had dynamic scope, which was confusing and slow.)
  • Why do closures matter outside of fancy functional code?
    Event handlers (a click handler that needs access to a counter), partial application, private state without classes, currying. Anywhere a function needs to remember something between calls without a class.
  • What is the loop variable gotcha in closures?
    In old JavaScript and some other languages, a closure created inside a loop captures the loop variable itself, not its value at iteration time — so all closures end up seeing the final value. Fixed by declaring with let (block-scoped) or capturing immediately.