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
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.
setTimeoutis a built-in function that runs a callback once after a delay in milliseconds.setTimeout(f, 1000)runsfafter 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:
setTimeoutcallbacks, 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-
setTimeoutquestion below.
The Waiter's Notepad — An Analogy
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
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 worksStep by step, what actually happens:
makeRoomKey()is called. A new Execution Context is pushed.roomNumberis created inside its Lexical Environment.- The inner function
showRoomis created. At creation time, it silently attaches a hidden link tomakeRoomKey's Lexical Environment — this link is the closure. makeRoomKey()returnsshowRoom, and its Execution Context is popped off the call stack (Lesson 2 — it is gone from the stack).- Normally, everything inside a popped context is garbage-collected. But
showRoomstill holds that hidden link toroomNumber— 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.) - When
showMyRoom()is finally called later, it readsroomNumberstraight from that still-alive Lexical Environment. Not a saved copy — the same variable.
Global's memory has makeRoomKey (hoisted) and showMyRoom (const, in its TDZ until this line finishes).
1function makeRoomKey() {2let roomNumber = 305;34return function showRoom() {5console.log(roomNumber);6};7}89const showMyRoom = makeRoomKey();10showMyRoom(); // 305
- makeRoomKey{ …code }
- showMyRoom🔒 TDZ
- roomNumber—
- outer →Global
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:
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, 3Why 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.
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.
1for (var i = 1; i <= 3; i++) {2setTimeout(() => console.log(i), 1000);3}4// after 1 second: 4, 4, 4
1for (let i = 1; i <= 3; i++) {2setTimeout(() => console.log(i), 1000);3}4// after 1 second: 1, 2, 3
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
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.
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.
1function createCounter() {2let count = 0;3return {4increment: () => ++count,5getValue: () => count,6};7}89const a = createCounter();10const b = createCounter();11a.increment(); a.increment();12b.increment();13console.log(a.getValue(), b.getValue()); // 2 1
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
Paste the makeRoomKey example
Run
const showMyRoom = makeRoomKey();in the console, then set a breakpoint insideshowRoom.Call showMyRoom() and hit the breakpoint
Open the Scope panel while paused inside the function.
Look for a section named “Closure”
Chrome DevTools labels it by the outer function's name — you'll see
Closure (makeRoomKey)listingroomNumber: 305, proving it's still alive.Compare with the var loop example
Set a breakpoint inside the
setTimeoutcallback and watch all three callbacks share the exact sameiin 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
TypeScript1. A private counter
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()); // 2count 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)
function createDiscountApplier(discountPercent: number): (price: number) => number {
return (price: number): number => price - (price * discountPercent) / 100;
}
const applyTenPercentOff = createDiscountApplier(10);
console.log(applyTenPercentOff(500)); // 450applyTenPercentOff permanently remembers discountPercent = 10, no matter how many different prices you later pass it.
3. A tiny hotel room object with private status
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 occupiedThree 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