Lesson 10 · 13 min read

Closures

A function that remembers where it was born — even after the place it was born in is gone. This is the one exception to "popped execution context = memory freed" that Lesson 2 promised we'd come back to.

On this page

  1. Concept
  2. Key words in plain English
  3. Why this matters
  4. The waiter's notepad — an analogy
  5. How a closure actually forms
  6. The classic loop bug — var vs let
  7. What closures are actually used for
  8. Closures and memory
  9. See it live — DevTools
  10. Common misconceptions
  11. Interview questions
  12. Three simple examples in TypeScript

Concept

A closure is a function bundled together with the variables it was surrounded by when it was created — its outer Lexical Environment (Lesson 7). Even after the outer function has finished running, returned a value, and been popped off the call stack, the inner function can still reach into those variables — not a copy of them, the actual, live variables.

Formally: a closure = a function + a live reference to its outer Lexical Environment. That reference is created the moment the function is defined, not when it's called.

Key Words In Plain English

  • Closure is a function together with the variables from the place where it was created. It keeps using those variables even after that place has finished running.
  • Lexical Environment is the engine's internal record of the variables of one scope, plus a link to the outer scope. “Lexical” means decided by where the code is written.
  • Execution Context is the engine's working area for one running piece of code. Each function call gets a new one.
  • Call stack is the list of function calls that are running right now. The newest call is on top. When a call finishes, it is removed from the top.
  • Garbage collection is the engine automatically freeing memory that nothing can reach any more.
  • Callback is a function you give to other code so that it can call your function later, for example when a timer ends.
  • setTimeout is a built-in function that runs a callback once after a delay in milliseconds. setTimeout(f, 1000) runs f after about one second.
  • Event listener is a callback that the browser calls when something happens, such as a click.
  • Encapsulation means keeping data hidden inside and allowing access only through chosen functions.
  • Function factory is a function that builds and returns other functions.
  • Memoization is remembering the result of a function call, so the same call later returns the saved result instead of working it out again.
  • Debounce is a way to wait until calls stop for a short time before running the function once. Throttle is a way to run the function at most once in a set period.
  • Memory leak is memory that your program no longer needs but that is still kept because something still points to it.

Why This Matters

  • It directly contradicts the simple mental model from Lesson 2 (“popped context, memory freed”) — closures are the reason that model needed an asterisk.
  • It's how JavaScript gives you private data without classes — a variable no one outside can touch directly, only through a function you handed them.
  • It quietly powers things you already use: setTimeout callbacks, event listeners, useState-style patterns, debounce/throttle utilities, memoization.
  • It's one of the most asked interview topics in JavaScript — usually through the loop-and-setTimeout question below.

The Waiter's Notepad — An Analogy

The guest checks out. The notepad doesn't forget the room.

A waiter takes a room-service order: Room 305, two coffees, no sugar. He writes it on his notepad and walks off to the kitchen. By the time he's back, the guest has already checked out — the front desk has closed that guest's file completely.

But the waiter's notepad still says Room 305. He doesn't need the front desk's file anymore — he's carrying his own private reference to that exact room number, and he can act on it whenever he's ready.

The outer function (the guest's stay) is over. The inner function (the waiter carrying his notepad) still remembers exactly what it needs — because it kept its own reference, not a phone call back to a desk that no longer has the file.

How A Closure Actually Forms

closure.js
function makeRoomKey() {
  let roomNumber = 305; // lives in makeRoomKey's Lexical Environment

  return function showRoom() {
    console.log(roomNumber);
  };
}

const showMyRoom = makeRoomKey(); // makeRoomKey() has already run, returned, and popped off the stack
showMyRoom(); // 305 — still works

Step by step, what actually happens:

  1. makeRoomKey() is called. A new Execution Context is pushed. roomNumber is created inside its Lexical Environment.
  2. The inner function showRoom is created. At creation time, it silently attaches a hidden link to makeRoomKey's Lexical Environment — this link is the closure.
  3. makeRoomKey() returns showRoom, and its Execution Context is popped off the call stack (Lesson 2 — it is gone from the stack).
  4. Normally, everything inside a popped context is garbage-collected. But showRoom still holds that hidden link to roomNumber — so the engine keeps that specific Lexical Environment alive in memory, even with nothing left on the stack pointing to it. (Real engines such as V8 are smart here: they keep only the variables the inner function actually uses, not every variable of the outer one.)
  5. When showMyRoom() is finally called later, it reads roomNumber straight from that still-alive Lexical Environment. Not a saved copy — the same variable.
step 1 / 7
Running
Line 9 — makeRoomKey() is called

Global's memory has makeRoomKey (hoisted) and showMyRoom (const, in its TDZ until this line finishes).

closure.js
1function makeRoomKey() {
2 let roomNumber = 305;
3
4 return function showRoom() {
5 console.log(roomNumber);
6 };
7}
8
9const showMyRoom = makeRoomKey();
10showMyRoom(); // 305
(console)
Call stack
Global ECrunning
  • makeRoomKey{ …code }
  • showMyRoom🔒 TDZ
Heap · memory that outlives the stack
makeRoomKey's Lexical Environmentnot yet
  • roomNumber—
  • outer →Global
↑ closure link · [[Environment]]
showRoom — not created yet
 
Step through it. Watch the makeRoomKey context leave the stack while its Lexical Environment stays on the heap — held by nothing but the returned function's link. · interactive
Call stack
Global EC
makeRoomKey() — popped
Memory (kept alive)
makeRoomKey's Lexical Environment
roomNumber = 305
kept alive — still referenced
↑ closure link
showRoom (a.k.a. showMyRoom)
The stack has moved on — but the returned function's closure link keeps roomNumber's home alive in memory. · closure link

The Classic Loop Bug — var vs let

This is the single most common closures interview question, and it's really a Lesson 9 (scoping) problem wearing a closures costume:

loop.js
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
// after 1 second: 4, 4, 4

for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
// after 1 second: 1, 2, 3

Why the var version breaks: var is not block-scoped (Lesson 9) — there is only one i for the entire loop. All three arrow functions close over that same single i. By the time any of them actually runs (a full second later), the loop has already finished and i is 4. All three closures read the same final value.

Why the let version works: let creates a fresh binding of i for every single iteration of the loop — three separate, independent variables, each living in its own block scope. Each arrow function closes over its own private copy. That's why let loops correctly are the default choice for callbacks inside loops.

step 1 / 8
Loop running · callbacks created
Iteration 1 — a callback is created and handed to setTimeout

The arrow function is created now, so its closure link is attached now. var: it links to the loop's single i (currently 1). let: it links to iteration 1's own block binding, i = 1.

clock: 0 ms
var · one shared i
1for (var i = 1; i <= 3; i++) {
2 setTimeout(() => console.log(i), 1000);
3}
4// after 1 second: 4, 4, 4
bindings of i · 1
function scope · i1
pending timers · each closes over…
callback 1→ the one i
(console)
let · one i per iteration
1for (let i = 1; i <= 3; i++) {
2 setTimeout(() => console.log(i), 1000);
3}
4// after 1 second: 1, 2, 3
bindings of i · 1
iteration 1 · i1
pending timers · each closes over…
callback 1→ iteration 1's i
(console)
Step through both loops together. Count the bindings: var has one, so all three callbacks read the same final 4; let has three, so each callback reads its own. · interactive

This is the loop-bug question interviewers love: it forces you to prove you understand block scope (Lesson 9) and closures at the same time.

What Closures Are Actually Used For

1. Private data, without a class

counter.js
function createCounter() {
  let count = 0; // nobody outside can touch this directly
  return {
    increment: () => ++count,
    getValue: () => count,
  };
}

There is no way to reach count from outside except through increment and getValue. The variable is genuinely private — closures gave you encapsulation for free.

step 1 / 5
Outer function called · new environment
createCounter() → environment A

A Lexical Environment with count = 0, and two functions linked to it. The context is popped; the environment survives because a's functions reference it.

counter.js
1function createCounter() {
2 let count = 0;
3 return {
4 increment: () => ++count,
5 getValue: () => count,
6 };
7}
8
9const a = createCounter();
10const b = createCounter();
11a.increment(); a.increment();
12b.increment();
13console.log(a.getValue(), b.getValue()); // 2 1
(console)
Heap · kept-alive environments
environment A · held by a
count0
↑ increment, getValue link here
Step through two counters. Each call to createCounter builds its own environment; each returned function is wired to exactly one of them. · interactive

2. Function factories

A function that builds and returns other, more specific functions — each one remembering the settings it was built with. (Full example in the TypeScript section below.)

3. Callbacks that need context

Every setTimeout, every event listener, every .then() — all of them are closures, quietly carrying whatever variables they need from where they were defined to whenever they eventually run.

Closures and Memory

Lesson 2 said popped execution contexts get garbage-collected. Closures are the reason that's not the whole story: as long as something still holds a reference to a closure (a variable, an event listener, a pending timer), the outer Lexical Environment it depends on is kept alive — even though its Execution Context left the call stack long ago.

This is genuinely useful, but it's also a real source of memory leaks in production apps — an event listener that's never removed keeps its closure (and everything the closure uses) alive for as long as the listener stays attached. Once nothing references a closure anymore, it becomes eligible for garbage collection like anything else.

See It Live — DevTools

  1. Paste the makeRoomKey example

    Run const showMyRoom = makeRoomKey(); in the console, then set a breakpoint inside showRoom.

  2. Call showMyRoom() and hit the breakpoint

    Open the Scope panel while paused inside the function.

  3. Look for a section named “Closure”

    Chrome DevTools labels it by the outer function's name — you'll see Closure (makeRoomKey) listing roomNumber: 305, proving it's still alive.

  4. Compare with the var loop example

    Set a breakpoint inside the setTimeout callback and watch all three callbacks share the exact same i in their Closure section.

Common Misconceptions

“A closure copies the variable's value.” No — it keeps a live reference. If the outer variable changes after the closure is created, the closure sees the new value, not a frozen snapshot.

“Closures only happen when a function is returned.” Returning is just the most visible case. Passing a function to setTimeout, an event listener, or anywhere else it outlives its original call also creates and relies on a closure.

“Every function call creates a new closure over the same shared data.” Each call to an outer function creates a brand-new, independent Lexical Environment. Two separate calls to createCounter() produce two completely independent counters that never interfere with each other.

Interview Questions

What is a closure, in your own words?

A function combined with a live reference to the Lexical Environment it was created in, letting it access those variables even after the outer function has finished executing.

Why does a var loop with setTimeout log the final value three times, while let logs 1, 2, 3?

var creates one shared variable for the whole loop, so every callback closes over that single, final value. let creates a fresh, independent binding per iteration, so each callback closes over its own value.

Do two calls to the same outer function share a closure?

No — each call creates its own separate Lexical Environment, so each returned closure has its own independent copy of the outer variables.

Does a closure store a value or a reference?

A reference to the variable's actual location in memory — not a snapshot of its value at creation time.

Can closures cause memory leaks?

Yes — if something keeps referencing a closure (like an event listener that's never removed), the variables it depends on are kept alive in memory for as long as that reference exists.

Three Simple Examples In TypeScript

TypeScript

1. A private counter

counter.ts
function createCounter(start: number): () => number {
  let count = start;
  return function (): number {
    count += 1;
    return count;
  };
}

const counter = createCounter(0);
console.log(counter()); // 1
console.log(counter()); // 2

count is only reachable through the returned function — there's no other way to read or change it from outside.

2. A function factory (discount applier)

discount.ts
function createDiscountApplier(discountPercent: number): (price: number) => number {
  return (price: number): number => price - (price * discountPercent) / 100;
}

const applyTenPercentOff = createDiscountApplier(10);
console.log(applyTenPercentOff(500)); // 450

applyTenPercentOff permanently remembers discountPercent = 10, no matter how many different prices you later pass it.

3. A tiny hotel room object with private status

room.ts
function createHotelRoom(roomNumber: number) {
  let isOccupied = false; // private — no direct access from outside

  return {
    checkIn: (): void => { isOccupied = true; },
    checkOut: (): void => { isOccupied = false; },
    status: (): string => `Room ${roomNumber} is ${isOccupied ? "occupied" : "free"}`,
  };
}

const room305 = createHotelRoom(305);
room305.checkIn();
console.log(room305.status()); // Room 305 is occupied

Three separate closures — checkIn, checkOut, and status — all sharing the same private isOccupied variable through the same Lexical Environment.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series