Introduction
If you've been writing JavaScript for some time now, you've probably used a closure without realizing it, maybe in a setTimeout, an event listener, or a function that returns another function. Closures are one of those concepts that feel abstract in theory but are everywhere in practice once you know what to look for.
They also have a reputation for being confusing, mostly because the confusion isn't about closures themselves, but how JavaScript handles variables and function calls in general. Once you understand this, closures stop feeling like a special trick and start feeling like a natural consequence of how the language works.
This article explains what a closure is, why it's useful, and goes through a real example showing how closures behave in practice.
What Is a Closure?
A closure is what happens when a function "remembers" the variables from the place it was created, even after that outer place has finished running. What does this mean?, In JavaScript, every function keeps a reference to the environment it was born in, this is called it's lexical scope. A closure is simply an inner function bundled together with that lexical scope. Put in another way: a closure lets a function keep access to variables from it's parent function, even after the parent has returned.
Here's a simple example:
function makeCounter() {
let x = 0;
return function () {
x++;
console.log(x);
};
}
const increment = makeCounter();
increment(); // 1
increment(); // 2
Looking at this code, you can see we are immediately calling makeCounter(), and assigning the value it returns (i.e the inner function) to the increment variable. Normally, you would expect x not to exist once makeCounter() finishes executing, but because the returned function references x, JavaScript keeps x alive in memory. So each time you call increment(), it's working with the same x value, not a fresh one.
Why do Closures matter?
To appreciate why closures are useful, it helps to first understand the default behavior they override.
The default behaviour: variables don't survive a function call
Every time a function is invoked, JavaScript creates a brand new execution context, and any variable(s) declared inside that function with let, const, or var gets recreated. The function becomes a brand new function, so nothing is remembered from previous calls.
function counter() {
let x = 0;
x++;
console.log(x);
}
counter(); // 1
counter(); // 1 (not 2 because x is recreated each time)
Each call to counter() is a new function call and recreates the x variable. If you want state to persist between calls, you need somewhere for that state to live outside the function itself, either as a global variable (which has downsides like being accessible and mutable from anywhere) or via a closure (whereby the variable exists within the scope of the function)
The solution: Closures
Closures solve this problem by letting an inner function "close over" a variable in it's parent scope. What this means is that the variable persists for as long as the inner function exists. This gives you:
- Persistence: State survives across multiple calls to the returned function.
- Privacy: The variable isn't accessible from outside, it can only be read or changed through the functions that close over it.
- Encapsulation: You control exactly how that hidden state can be modified, similar to a private field in object-oriented programming.
This pattern shows up constantly in real JavaScript code: counters, caches, memoized functions, event handlers with private state, and the module pattern all rely on closures.
How to use Closures
The mechanics of closures often times confuses people who don't understand it properly. Creating a closure and using it are two different things. Let's walk through an example that illustrates where things can go wrong.
A closure that's never triggered
document.querySelector("#btn").addEventListener("click", getScore);
function getScore() {
let score = 0;
return function () {
score++;
console.log(score);
};
}
At first glance, this looks like it should increment score by 1 when the button is clicked, and log out the score to the console, but it doesn't. The reason is a subtle but important one, addEventListener("click", getScore) registers getScore itself as the click handler, not the function that getScore returns. So on every click:
- getScore runs.
- score is created afresh.
- A new inner function is created and returned, but addEventListener doesn't do anything with the returned value, so the returned function just vanishes.
- The inner function that increments score by 1 is never called.
- score stays as 0, and is discarded the moment getScore finishes.
This repeats identically on every new click: a new score, a new inner function that's discarded, and no increment happening. The closure exists for a fraction of a second and is discarded before it's ever used.
The fix: Calling the outer function first
To actually use the closure, you need to invoke the outer function yourself so that it's returned inner function, the one that closes over score, becomes the event handler:
document.querySelector("#btn").addEventListener("click", getScore());
function getScore() {
let score = 0;
return function () {
score++;
console.log(score);
};
}
Notice the difference: getScore() is called immediately. This runs getScore immediately without waiting for the event to fire, and it's returned value, the inner function, is what gets registered as the function that will run when the click event fires. Now:
-
scoreis created only once, whengetScore()runs. - The inner function closes over that specific score.
- Every click calls the inner function, which will increment score by 1, and can read or update it, and score persists across clicks because it never gets recreated.
The general pattern
The 2 pieces to watch out for whenever you're using closures:
- An outer function – Declares a variable in its local scope.
- An inner function – Defined inside the outer one, references that variable.
The inner function is returned or passed along somewhere it can be called later, as an event handler, a callback, or stored in a variable. Crucially, it's the returned inner function that needs to be invoked or registered, not the outer function reference itself, or the closure never gets a chance to do anything.
Conclusion
A closure is a function paired with the scope it was created in, and it exists so that state can persist and stay private between calls without relying on global variables. The catch is that a closure only does useful work once it's inner function is actually the thing being called, give addEventListener (or anything else) the outer function by mistake, and every invocation just creates and discards a fresh, unused closure.
Once you're deliberate about which function you're passing around, closures become a natural way to build persistent, encapsulated state in JavaScript.
Top comments (1)
One thing that helped me with closures was to stop trying to memorize the definition and to just ask, “What does this function still have access to? ~
That accelerates the entire process significantly.
I would probably lean even harder into that mental model here, because once someone is over this, the examples with counters and event handlers are feel much less mysterious.