cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Closures in JavaScript

Understand how a function keeps access to its variables, even after the function that created them has finished.

Three things to take away
  • 01
    Retained variablesCall a function after its outer function has finished.
  • 02
    Shared or separate?Switch one line and watch two counters share state.
  • 03
    Private 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.

The short version

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.

Follow a closure, one line at a time
Step 0 of 5Ready
Your turn: follow the blue line

Click the blue line, or Step. Start at the call on line 7; a function body runs only when called.

Running in
  1. script
Next: line 7
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function createGreeting(message) {  return function greet() {    return message;  };} greet();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.

Editing starts a fresh walkthrough. Use Step or click the blue line to follow your new value.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Separate these two moments:

  1. Line 7 creates: createGreeting("Hello!") returns a function. We store that function in greet. The outer call is now finished.
  2. Line 8 calls: greet() runs the inner function, which still reads message from 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

INTERACTIVE

A 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 inside a counter
Step 0 of 15Ready
Your turn: follow the blue line

Step into each call. Watch count appear, survive the outer return, and change beside the code.

Running in
  1. script
Next: line 9
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function makeCounter() {  let count = 0;  return function increment() {    count += 1;    return count;  };} a();a(); const b = makeCounter();b();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Notice what did not happen

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().

Follow a new counter or a shared one
Step 0 of 16Ready
Your turn: follow the blue line

Step into each call. Watch count appear, survive the outer return, and change beside the code.

Running in
  1. script
Next: line 9
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function makeCounter() {  let count = 0;  return function increment() {    count += 1;    return count;  };} const b = makeCounter();a === b;a();b();a();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change line 10, then watch the path change

Changing the line starts a fresh walkthrough. With b = a, watch JavaScript skip the factory call.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Step inside the private cart
Step 0 of 9Ready
Your turn: follow the blue line

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.

Running in
  1. script
Next: line 14
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function createCart() {  let items = 0;  return {    add() {      items += 1;      return items;    },    getCount() {      return items;    }  };} cart.add();cart.items = 99;cart.getCount();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Watch a variable change, not a saved string
Step 0 of 11Ready
Your turn: follow the blue line

Follow the two variables beside lines 2 and 3. Step through the methods to see why only one result changes.

Running in
  1. script
Next: line 18
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

“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.

What about loops?

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 CHECK

Using the same makeCounter from the playground, what will these three calls return?

Predict before you runPop out in the code editor (opens in a new tab)JavaScript
const counter = makeCounter();
const alias = counter;

counter(); // ?
alias();   // ?
counter(); // ?
Choose an answer to see the explanation.

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.

CompleteFrontend Clear concepts. Working examples.