JavaScript closures let a function remember and access variables from its lexical scope even after the outer function has finished running. They power private variables, function factories, memoization, event handlers, and patterns used across modern JavaScript frameworks. A closure basically mixes scope, memory, and execution context into one extremely useful abstraction.
Introduction
Closures are one of those concepts everyone hears about, most people pretend to “kind of know,” and only a few actually understand properly. But once it clicks, your whole JavaScript game levels up — suddenly React hooks make sense, memoization stops sounding fancy, and your event handlers behave exactly how you expect.
To truly grasp closures, you need to understand lexical scoping, how execution contexts work under the hood, and why JavaScript sometimes hangs onto variables longer than you expect.
In this article, we’ll break closures down step-by-step and then walk through practical, real-world examples that you’ll actually use as a developer.
What Is a Closure?
A closure is formed when a function “remembers” variables from the environment where it was created — even after that environment is gone.
- The function itself
- The lexical environment (the surrounding scope)
- Any captured variables the function uses
As long as an inner function exists somewhere in your app, JavaScript keeps its surrounding environment alive.
Lexical Scoping and Execution Contexts
How Lexical Scoping Works
JavaScript uses lexical scope, which simply means where you write your function determines what variables it can access — not where you call it.
Execution Context Lifecycle
Every time a function runs, JS creates a fresh execution context containing:
- A variable environment
- A lexical environment linking to outer scopes
- The this binding
Normally, when a function returns, its context is destroyed. Unless a closure is involved — then JS keeps those variables alive.
Example 1: Basic Closure in Action
function outer() {
let count = 0; // captured variable
function inner() {
count++;
return count;
}
return inner;
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
`count` survives because `inner()` still references it. That’s closures in a nutshell.
Why Closures Persist Memory
JavaScript’s garbage collector cleans up memory only when things are unreachable. Here:
- `inner` still exists.
- `inner` uses `count`.
- JS keeps `count` alive.
Great for functionality, but can cause headaches if you’re not careful with long-lived closures.
Real-World Use Case 1: Private Variables and Encapsulation
Closures were the OG way to create private variables before JavaScript had `#private` fields.
Example: Private Counter Module
function createCounter() {
let value = 0; // private
return {
increment() {
value++;
return value;
},
getValue() {
return value;
}
};
}
const counter = createCounter();
counter.increment(); // 1
console.log(counter.getValue()); // 1
console.log(counter.value); // undefined
Real-World Use Case 2: Function Factories
Example: Create a Logger with Namespaces
function createLogger(prefix) {
return function(message) {
console.log(`[${prefix}] ${message}`);
};
}
const info = createLogger('INFO');
const debug = createLogger('DEBUG');
info('Server started');
debug('Cache miss occurred');
Real-World Use Case 3: Event Handlers and DOM State
Example: Button Click Counter
function attachClickCounter(button) {
let clicks = 0;
button.addEventListener('click', () => {
clicks++;
console.log(`Clicked ${clicks} times`);
});
}
attachClickCounter(document.querySelector('#myButton'));
Real-World Use Case 4: Memoization for Performance
Example: Basic Memoization Wrapper
function memoize(fn) {
const cache = {};
return function(key) {
if (key in cache) {
return cache[key];
}
const result = fn(key);
cache[key] = result;
return result;
};
}
function slowDouble(n) {
for (let i = 0; i < 1e7; i++);
return n * 2;
}
const fastDouble = memoize(slowDouble);
console.log(fastDouble(5)); // slow
console.log(fastDouble(5)); // fast
Closures and Loops: A Common Trap
Classic Bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3
Fix with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
Closures in Modern JavaScript
Closures are everywhere today — you probably use them daily without realizing it.
React Hooks
`useState`, `useCallback`, `useMemo` — all closure magic under the hood.
Async/Await
Even when async functions pause, they remember surrounding variables.
Functional Programming
Currying, partial application, higher-order functions — closures power all of it.
Performance Considerations
- Huge objects captured accidentally
- Event listeners that never get removed
- Memoization caches that grow forever
A closure that holds more than it needs is like a suitcase stuffed with junk you never unpack.
When NOT to Use Closures
- You need clean, stateless functions
- The captured scope is too large
- A class or module is simpler to maintain
FAQ
1. Are closures the same as scopes?
No. Scope decides what’s visible. A closure is a function bundled with its captured scope.
2. Do closures impact memory usage?
Yes. Captured variables stay in memory as long as the closure exists.
3. Can closures cause memory leaks?
Definitely, especially with long-lived listeners and unused references.
4. Are closures faster or slower?
Neither. The main cost is memory retention, not CPU.
5. Do all JavaScript functions create closures?
Technically yes, but a closure is only active when an inner function uses variables from an outer scope.


