Understanding JavaScript Closures (Without the Confusion)
Closures explained in plain English with practical examples. Stop memorizing, start understanding.
Closures are one of those concepts that sound scarier than they are. Let’s demystify them with examples you’ll actually use.
The One-Sentence Explanation
A closure is when a function remembers variables from its outer scope, even after that outer scope has finished executing.
That’s it. Everything else is details.
Example 1: The Classic Counter
function createCounter() {
let count = 0; // Private variable
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
What’s happening:
createCountercreates a variablecount- Returns a function that uses
count - Even after
createCounterfinishes, the inner function still has access tocount - That’s a closure!
Example 2: Private Variables
Closures let you create truly private data:
function createUser(name) {
let password = 'secret123'; // Private!
return {
getName() {
return name;
},
checkPassword(attempt) {
return attempt === password;
},
// No way to directly access password
};
}
const user = createUser('Kevin');
console.log(user.getName()); // 'Kevin'
console.log(user.checkPassword('123')); // false
console.log(user.password); // undefined - can't access!
No one can see password from outside. It’s truly private.
Example 3: Event Handlers
This is where closures shine:
function setupButtons() {
const buttons = ['Login', 'Logout', 'Settings'];
buttons.forEach((label, index) => {
const button = document.createElement('button');
button.textContent = label;
// This function closes over 'label' and 'index'
button.addEventListener('click', () => {
console.log(`Button ${label} (${index}) clicked`);
});
document.body.appendChild(button);
});
}
Each click handler remembers its specific label and index.
The Common Mistake: Loop Closures
This doesn’t work as expected:
// ❌ Wrong - all click handlers log 3
for (var i = 0; i < 3; i++) {
const button = document.createElement('button');
button.textContent = i;
button.addEventListener('click', () => {
console.log(i); // Always logs 3!
});
}
Why? All handlers share the same i, which ends up as 3.
Fix 1: Use let
// ✅ Right - let creates new scope each iteration
for (let i = 0; i < 3; i++) {
const button = document.createElement('button');
button.textContent = i;
button.addEventListener('click', () => {
console.log(i); // Logs correct value!
});
}
Fix 2: IIFE (Old school)
for (var i = 0; i < 3; i++) {
(function(index) {
const button = document.createElement('button');
button.textContent = index;
button.addEventListener('click', () => {
console.log(index);
});
})(i); // Pass i as argument
}
But really, just use let. It’s 2025.
Example 4: Module Pattern
Create modules with private state:
const ShoppingCart = (function() {
// Private state
let items = [];
// Private helper
function calculateTotal() {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Public API
return {
add(item) {
items.push(item);
},
remove(itemId) {
items = items.filter(item => item.id !== itemId);
},
getTotal() {
return calculateTotal();
},
getItems() {
return [...items]; // Return copy, not reference
}
};
})();
ShoppingCart.add({ id: 1, price: 10 });
console.log(ShoppingCart.getTotal()); // 10
console.log(ShoppingCart.items); // undefined - private!
Clean API, no global pollution.
Example 5: Function Factory
Create specialized functions:
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Each function remembers its factor.
Example 6: Partial Application
Pre-fill function arguments:
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
const sayHello = partial(greet, 'Hello');
const sayHi = partial(greet, 'Hi');
console.log(sayHello('Kevin')); // 'Hello, Kevin!'
console.log(sayHi('Alex')); // 'Hi, Alex!'
React Example: Custom Hooks
Closures are everywhere in React:
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// These functions close over 'count' and 'setCount'
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
const decrement = useCallback(() => {
setCount(c => c - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
Memory Considerations
Closures keep variables alive:
function createHugeArray() {
const huge = new Array(1000000).fill('data');
return function() {
return huge.length; // Whole array stays in memory!
};
}
Solution: Only close over what you need:
function createHugeArray() {
const huge = new Array(1000000).fill('data');
const length = huge.length; // Cache what you need
return function() {
return length; // Only closes over 'length'
};
// 'huge' can be garbage collected
}
When to Use Closures
✅ Good use cases:
- Private variables
- Event handlers
- Callbacks
- Factory functions
- Partial application
- Module pattern
❌ When not to use:
- When you don’t need private state
- When simpler alternatives exist
- When memory is tight (mobile)
The Mental Model
Think of closures as a backpack:
function outer() {
const item = 'lunch'; // Put item in backpack
function inner() {
console.log(item); // Still have backpack
}
return inner;
}
const fn = outer(); // Take backpack with you
fn(); // Open backpack, use item
The inner function carries its environment (backpack) wherever it goes.
Debugging Closures
Chrome DevTools shows closure scope:
function createCounter() {
let count = 0;
return function() {
debugger; // Pause here
count++;
return count;
};
}
const counter = createCounter();
counter(); // Check "Closure" in DevTools
You’ll see count in the Closure scope!
Common Interview Questions
Q: What’s a closure? A: A function that has access to variables from its outer scope, even after that scope has executed.
Q: Why are closures useful? A: Data privacy, event handlers, callbacks, factory functions.
Q: What’s the difference between closure and scope? A: Scope is rules for variable access. Closure is when a function remembers its scope.
Conclusion
Closures aren’t magic. They’re just:
- Functions can access outer variables
- Functions can be returned/passed around
- Functions remember their environment
That’s it. Everything else builds on these basics.
Practice by creating:
- A counter (private state)
- Event handlers (remember data)
- A module (private functions)
Once you’ve built these, closures will click.
Now go close over something! 🎯
Keep reading
Why I'm Using Moondream Instead of Cloud Vision APIs
Moondream vs GPT Vision vs Gemini: local vision AI that runs on your laptop. Free image captioning, OCR, object detection, and visual Q&A without API costs. Includes Python examples and GitHub repo.
Computer Vision
Maybe LLM CLI Is All You Need.
Boost developer productivity with command-line AI tools. Learn Simon Willison's LLM CLI, build custom translation tools, and automate git commits. Practical examples for terminal-based AI workflows.
Developer Tools
LLM Data Extraction: A Complete Guide to Document Processing Libraries and Tools
Master document processing for LLMs with this comprehensive guide covering open-source libraries, premium APIs, and cloud services. Compare PyMuPDF, Unstructured.io, Docling, and LlamaParse for your RAG systems.
AI Engineering