Closures and Scope
Reviewed & published by Brayan K
A clear, in-depth guide to closures in JavaScript — with worked examples, common pitfalls, and a practice quiz.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Understand lexical scope vs dynamic scope
- How functions 'remember' variables
- Create private variables and encapsulation
- Function factories and currying
- Common closure pitfalls (loops)
- Real-world use cases in modern apps
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
🎒 Real-World Analogy: A closure is like a backpack that remembers:
- • When a function is created, it packs a "backpack" with all the variables around it
- • Even when the function travels far away (like being passed to another function), it carries its backpack
- • The function can always reach into its backpack and use those remembered variables
- • This is how functions "remember" data even after their parent function has finished!
INTRODUCTION — Why Closures Matter More Than Anything Else in JavaScript
Closures are the heart and soul of JavaScript.
| Closure Use Case | What It Does | Real Example |
|---|---|---|
| Private variables | Hide data from outside access | Counter that can't be reset |
| Function factories | Create customized functions | makeMultiplier(5) |
| Event handlers | Remember context when clicked | Button click with ID |
| React hooks | useState, useEffect internals | Component state |
- Private variables
- Function factories
- Event handlers
- State management
- Asynchronous behavior
- Module patterns
- Encapsulation
- Secure data control
Every professional JavaScript developer — especially those working in React, Node.js, or complex web apps — uses closures every single day, often without noticing.
By the end of this lesson, closures will feel completely natural.
SECTION 1 — Understanding SCOPE (Global, Function, Block, Lexical)
Before closures make sense, you MUST understand scope.
Scope = where variables can be accessed.
JavaScript uses Lexical Scope, meaning:
📌 Variables are resolved based on WHERE the code was written, not where it is executed.
1. Global Scope
Anything declared outside a function is global.
Accessible everywhere. Dangerous for large apps.
2. Function Scope
var variables live inside the function only.
Function boundaries matter a lot in closures.
3. Block Scope (let / const)
Block scoping is essential for fixing closure bugs (you'll see later).
4. Lexical Scope (Most Important)
Inner functions access variables declared where they were written.
Lexical scope is the engine behind closures.
SECTION 2 — What Is a Closure? (Simple Definition)
A function that "remembers" variables from its outer scope — even after the outer function has returned.
This is the single most important concept in JavaScript.
The Classic Example:
Even though outer() has finished executing, the inner function keeps access to:
- inner scopes
Closures keep data alive.
SECTION 3 — The Real Superpower: Private Variables
JavaScript has no private keyword (until ES2020 classes). Before that, closures were the ONLY way to hide data.
Example: Private Counter
This is true encapsulation — impossible with global variables.
SECTION 3b — Worked Example: A Wallet With a Private Balance
Here is the counter idea turned into something you would actually ship. Read the whole program first — every non-obvious line has a comment telling you what it does and why — then press Run and check the output matches the comments.
The thing to watch for: createWallet finishes running on the line where you call it, yet balance is still alive several lines later. That is the closure doing its job.
// -- WORKED EXAMPLE - a closure you can watch working --
function createWallet(ownerName, startingBalance) {
// 'balance' is PRIVATE: it lives inside createWallet and nothing outside
// this function can reach it directly. Only the functions below can.
let balance = startingBalance;
// Each function below is written INSIDE createWallet, so each one packs a
// "backpack" holding ownerName and balance. That backpack is the closure.
return {
deposit(amount) {
balance = balance + amount; // reaches into the backpack
return ownerName + " deposited " + amount + ". Balance: " + balance;
},
withdraw(amount) {
if (amount > balance) { // guard: never go negative
return "Declined - " + ownerName + " only has " + balance;
}
balance = balance - amount;
return ownerName + " withdrew " + amount + ". Balance: " + balance;
},
getBalance() {
return balance; // read-only view of the private value
}
};
}
// createWallet() runs once and FINISHES on this line...
const wallet = createWallet("Ada", 100);
// ...but 'balance' stays alive, because the three returned functions remember it.
console.log(wallet.deposit(50)); // Ada deposited 50. Balance: 150
console.log(wallet.withdraw(30)); // Ada withdrew 30. Balance: 120
console.log(wallet.withdraw(999)); // guard fires, balance is left alone
// The private variable really is unreachable from outside:
console.log(wallet.balance); // undefined - there is no 'balance' property
console.log(wallet.getBalance()); // 120 - the only legitimate way to read it
// Every call to createWallet builds a BRAND NEW backpack, not a shared one:
const wallet2 = createWallet("Grace", 10);
console.log(wallet2.getBalance()); // Grace's own balance
console.log(wallet.getBalance()); // Ada's is untouched
// Expected output:
// Ada deposited 50. Balance: 150
// Ada withdrew 30. Balance: 120
// Declined - Ada only has 120
// undefined
// 120
// 10
// 120Try breaking it on purpose: change let balance to const balance and run again. You get TypeError: Assignment to constant variable. — proof that deposit really is writing to the outer variable, not to a copy.
SECTION 4 — Function Factories (Closures Generating Functions)
Closures let you preload data into functions.
Make a Function That Remembers a Number (Multiplier):
- dynamic validators
- React hook generators
- custom loggers
- configuration wrappers
- dependency injection
🎯 Your Turn — Finish the Closures
Everything below is written for you except two blanks. Both blanks are about the one idea this lesson teaches: an inner function reaching a variable from the function that created it. Fill them in and press Run.
// 🎯 YOUR TURN - fill in the blanks marked ___
// PART 1 - a function factory
function makeLogger(label) {
// The function you return is written INSIDE makeLogger, so it can remember
// 'label' forever - even after makeLogger has finished running.
return function (message) {
// 👉 1) Replace ___ with the variable the outer function remembers.
return "[" + ___ + "] " + message;
};
}
const errorLog = makeLogger("ERROR"); // this logger remembers "ERROR"
const infoLog = makeLogger("INFO"); // a separate backpack, remembering "INFO"
console.log(errorLog("Disk full"));
console.log(infoLog("Backup finished"));
// PART 2 - a private counter
function createCounter() {
// 👉 2) Replace ___ with the keyword that makes 'hits' private to this
// function AND lets it be reassigned. (Not const. Not var.)
___ hits = 0;
return {
hit() {
hits = hits + 1; // reaches into the closure and updates it
return hits;
},
total() {
return hits; // the only way to read the private value
}
};
}
const counter = createCounter();
counter.hit();
counter.hit();
counter.hit();
console.log("Total hits:", counter.total());
console.log("Direct access:", counter.hits); // stays hidden
// ✅ Expected output:
// [ERROR] Disk full
// [INFO] Backup finished
// Total hits: 3
// Direct access: undefinedStuck on blank 2? The last line is the clue: counter.hits must print undefined. If you pick a keyword that puts hits on the returned object, that line would print 3 instead.
SECTION 5 — Closures and Event Handlers
Event handlers, timeouts, and interactive websites heavily rely on closures.
Example:
Even after setupButton finishes, the inner arrow function still knows:
- surrounding variables
- the DOM element
That's why closures are essential for UI programming.
SECTION 6 — Closures + Async Code (Critical for Modern JS)
Closures keep variables alive during async operations.
Even though loadUser has returned long before the API finishes, the .then() callback remembers prefix.
This is EXACTLY how:
- Node.js callbacks
all retain access to old variables.
SECTION 7 — Memoization with Closures (Speed Boost Technique)
Memoization = caching results so future calls are instant.
Closures make memoization easy.
SECTION 8 — SUMMARY & CHEAT SHEET
const global = 1;function() { var x = 1; }if (true) { let x = 1; } // x not accessible outsidefunction outer() { const x = 1; function inner() { console.log(x); } }📋 Quick Reference — Closures
| Concept | Pattern |
|---|---|
| Basic Closure | function outer() { return function inner() {} } |
| Private Data | let count = 0; return { inc: () => ++count } |
| Factory | makeAdder(5)(10) // 15 |
| Memoization | Cache results inside closure scope |
| Event Handler | btn.onclick = () => log(id) |
🏆 Mini-Challenge: Write once()
No blanks this time — just a brief and an outline. Write a function called once(fn). It takes a function and hands you back a new function that runs fn only the first time it is called. Every later call returns the value from that first run without calling fn again.
This is a real utility — libraries use it for one-time setup, and it is impossible to write without a closure, because the "have I run yet?" flag has to survive between calls while staying hidden from everyone else.
// 🎯 MINI-CHALLENGE: write once(fn)
//
// Outline - no logic filled in, that part is yours:
//
// function once(fn) {
// 1. create a private flag 'called', starting as false
// 2. create a private 'result' to hold the first return value
// 3. return a function that accepts ...args
// 4. if called is still false: set called = true and store fn(...args) in result
// 5. return result (the same value every single time)
// }
//
// Until you have written once(), running this throws
// "ReferenceError: once is not defined" - that is expected, not a bug.
// your code here
// --- test harness: do not change anything below this line ---
let runs = 0;
const setup = once(function (name) {
runs = runs + 1; // counts how many times the REAL work happened
return "Configured " + name;
});
console.log(setup("app"));
console.log(setup("app"));
console.log(setup("something else")); // ignored - the first result is reused
console.log("times the real function ran:", runs);
// ✅ Expected output:
// Configured app
// Configured app
// Configured app
// times the real function ran: 1Note the third line of expected output: setup("something else") still prints Configured app. That is the point — the argument is ignored because fn never runs a second time.
Lesson Complete — Closures & Scope!
You've mastered one of the hardest concepts in JavaScript. Understanding closures puts you in the top tier of JavaScript developers.
Practice quiz
What is a closure in JavaScript?
- A way to close a browser window
- A function with no parameters
- A function that remembers variables from its outer scope even after the outer function has returned
- A loop that runs forever
Answer: A function that remembers variables from its outer scope even after the outer function has returned. The lesson defines a closure as a function that remembers variables from its outer scope, even after the outer function has returned.
What kind of scope does JavaScript use to resolve variables?
- Lexical scope
- Dynamic scope
- Random scope
- Block-only scope
Answer: Lexical scope. JavaScript uses lexical scope: variables are resolved based on WHERE the code was written, not where it is executed.
Given: function outer() { const name = 'Boopie'; function inner() { console.log(name); } return inner; } outer()(); What does this log?
- undefined
- ReferenceError
- null
- 'Boopie'
Answer: 'Boopie'. Through lexical scope, inner() accesses name from where it was written, so calling outer()() logs 'Boopie'.
In the createCounter example, what does counter.count return after two increments?
- 2
- undefined
- 0
- 1
Answer: undefined. count is a private variable trapped in the closure, so counter.count returns undefined; only the returned methods can reach it.
Which statement about a 'var' variable declared inside a function is correct?
- It lives inside the function only (function scope)
- It is global
- It is block scoped to the nearest if
- It cannot be reassigned
Answer: It lives inside the function only (function scope). var variables live inside the function only, which is why console.log(x) outside the function throws an error.
After this classic loop with var: var fns = []; for (var i = 0; i < 3; i++) { fns.push(() => i); } What do fns[0](), fns[1](), fns[2]() return?
- 0, 1, 2
- 0, 0, 0
- 3, 3, 3
- undefined, undefined, undefined
Answer: 3, 3, 3. All callbacks share the same var i, which is 3 after the loop ends, so every call returns 3.
If you change the loop to use 'let' instead of 'var': for (let i = 0; i < 3; i++) { fns.push(() => i); } What do the three functions return?
- 3, 3, 3
- 0, 1, 2
- undefined
- 1, 2, 3
Answer: 0, 1, 2. let creates a fresh binding of i per iteration, so each closure captures its own value: 0, 1, 2.
In makeMultiplier, given const triple = makeMultiplier(3), what does triple(10) return?
- 13
- 3
- 10
- 30
Answer: 30. The returned function closes over factor = 3, so triple(10) computes 10 * 3 = 30.
What is the purpose of the cache object inside the memoize closure?
- To delete old functions
- To store results so repeated calls with the same arguments return instantly
- To make the code run slower
- To convert numbers to strings permanently
Answer: To store results so repeated calls with the same arguments return instantly. Memoization caches results in the closure's cache object so future calls with the same arguments are instant.
Why can event handlers and async callbacks (like .then or setTimeout) still access variables from a function that already returned?
- Because JavaScript copies the whole program into them
- Because the variables become global automatically
- Because closures keep those outer variables alive after the outer function returns
- Because async code disables scope
Answer: Because closures keep those outer variables alive after the outer function returns. Closures keep the outer scope's variables alive, so handlers and async callbacks (e.g., .then remembering prefix) still access them after the outer function returns.
Continue this course
- Previous: Error Handling
- Next: Prototypes and Classes — Model real-world objects with JS classes and prototype inheritance
- Quick reference: JavaScript cheat sheet
- From the blog: Understanding JavaScript Closures