Closures in JavaScript
Understand how a function keeps access to its variables, even after the function that created them has finished.
- 01Retained variablesCall a function after its outer function has finished.
- 02Shared or separate?Switch one line and watch two counters share state.
- 03Private by designTry to overwrite a cart’s private item count.
What is a closure?
A closure lets a function keep using variables from the place where it was created. An outer function can finish, and a function created inside it can still read those variables later.
We’ll start with a function that remembers a short message. Then we’ll use a counter to see that a remembered variable can change too.
A function remembers where it was defined, not just where it is called.
You don’t need special syntax to create a closure. JavaScript functions form closures when they are created. Returning a function simply makes that behavior easier to observe.
Click the blue line, or use Step, to follow the code one event at a time. The app explains each jump into a function, each variable change, and each return. Use Back whenever you want to compare what just changed.
Start with a nested function
Nested just means one function is written inside another. The inner function can read variables from the outer one. JavaScript calls this lexical scope.
In this example, message is the text "Hello!" passed to createGreeting. That outer function returns greet, an inner function that reads the text. Step inside the first call, watch it return, then follow the second call back to the remembered variable.
Click the blue line, or Step. Start at the call on line 7; a function body runs only when called.
script
function createGreeting(message) { return function greet() { return message; };} greet();Editing starts a fresh walkthrough. Use Step or click the blue line to follow your new value.
Separate these two moments:
- Line 7 creates:
createGreeting("Hello!")returns a function. We store that function ingreet. The outer call is now finished. - Line 8 calls:
greet()runs the inner function, which still readsmessagefrom that finished outer call.
That continued access to message is the closure. The variable did not need to be global, and we did not pass it into greet().
See a closure in action
INTERACTIVEA greeting reads a variable. A counter goes one step further: it changes a variable and remembers the new value on the next call.
Step follows execution into each function instead of skipping straight to its result. Watch the count change beside its declaration. After the walkthrough, choose another call and follow that one too.
Step into each call. Watch count appear, survive the outer return, and change beside the code.
script
function makeCounter() { let count = 0; return function increment() { count += 1; return count; };} a();a(); const b = makeCounter();b();Calling a() a second time did not run makeCounter() again. It ran the returned function, which updated the same count from 1 to 2.
One factory, separate memories
Each call to makeCounter() creates a new lexical environment with its own count. But assigning const b = a gives the same function another name. It does not call the factory again.
Follow a(), then b(), then a() again. Change line 10 and replay the path. With b = a, notice that JavaScript never makes the second trip into makeCounter().
Step into each call. Watch count appear, survive the outer return, and change beside the code.
script
function makeCounter() { let count = 0; return function increment() { count += 1; return count; };} const b = makeCounter();a === b;a();b();a();This distinction matters: a new factory call creates new state; another reference to the same function does not.
A practical use: private state
Imagine a shopping cart whose item count should only change through an add method. A closure lets you keep that state out of the global scope while exposing a small, intentional API.
Start with the empty cart. Follow the creation of items, then the update inside add(). At line 16, use Back and Step to compare the property assignment with the private variable that stays unchanged.
Click the blue line to follow the cart from creation to return. Each step explains one event, instead of jumping straight to the final numbers.
script
function createCart() { let items = 0; return { add() { items += 1; return items; }, getCount() { return items; } };} cart.add();cart.items = 99;cart.getCount();add and getCount are created in the same call to createCart, so they share one items binding. Neither method gets a frozen copy of its value.
The binding is not accessible as cart.items. Even assigning a new property with that name would not change the closed-over variable. Calling createCart() again would create a separate cart with separate state.
You’ll also see closures in event handlers that retain configuration, debounce functions that retain a timer, and callbacks that use values from their surrounding code.
Common misconceptions
A closure does not freeze every value, and it does not keep every expression up to date. Here, one function reads the current count; another reads a message string computed when the factory ran.
Follow the two variables beside lines 2 and 3. Step through the methods to see why only one result changes.
script
function createReaders() { let count = 0; const message = `Count is ${count}`; return { increment() { count += 1; return count; }, live() { return `Count is ${count}`; }, saved() { return message; } };} readers.increment();readers.live();readers.saved();“A closure stores a snapshot of a value.”
It retains access to a binding. In the experiment, count changes but message does not. Reading that unchanged string again cannot recompute the expression that created it.
“Every function gets its own independent state.”
Functions created within the same outer call can share variables, like the cart methods above. Separate calls to the factory create separate environments.
“You have to return a function to make a closure.”
No. Returning a function is one way to keep it reachable. Passing a nested function to an event listener or a timer can also retain its surrounding variables.
“A closure keeps everything alive forever.”
No. Retained state can become eligible for garbage collection when nothing reachable needs it. Long-lived callbacks can retain large objects, so remove unused listeners and clear timers when appropriate.
Callbacks inside a var loop share one loop-variable binding. With let in the loop header, each iteration gets its own binding. This is why delayed callbacks can print different results depending on which declaration you use.
Check your understanding
QUICK CHECKUsing the same makeCounter from the playground, what will these three calls return?
const counter = makeCounter();
const alias = counter;
counter(); // ?
alias(); // ?
counter(); // ?Key takeaways
- A closure is a function plus access to its surrounding lexical environment.
- The outer function can finish while the returned function still accesses its variables.
- Closures retain bindings, not frozen copies of values.
- Separate factory calls create separate state. Functions from the same call can share state.
Remember the relationship, not the jargon.
The function finishes. Its execution ends. The closure’s access to the variables can remain.