JavaScript Closures

In the previous lesson, we learned how to bundle data and behavior together and hide the inside details. Now let’s look at closures, the feature that makes that kind of privacy possible.

🧠 What is a Closure?

A closure is a function that remembers and keeps access to variables from the place where it was created, even after the outer function that made it has finished running.

To see this, you first need the idea of an inner function. When you define a function inside another function, the inner function can read the outer function’s variables. We will cover the full rules of which variables a function can see in the next lesson on scope, but the part that matters here is simple: the inner function stays connected to those variables even after the outer function returns.

closures.js
function makeGreeting(name) {
// `name` belongs to the outer function
return function () {
return `Hello, ${name}!`; // the inner function still remembers `name`
};
}
const greetAlex = makeGreeting("Alex");
console.log(greetAlex()); // Hello, Alex!

Let’s walk through how the inner function holds on to name:

  • function makeGreeting(name) is the outer function, and name is its local variable.
  • The return function () { ... } line hands back an inner function that reads name from the outer function around it.
  • const greetAlex = makeGreeting("Alex") runs the outer function with name set to "Alex", then stores the returned inner function in greetAlex.
  • greetAlex() calls that inner function later. Even though makeGreeting already finished, the inner function still reaches back to the name it was created with and returns Hello, Alex!.

That held-on connection between the inner function and the outer variable is the closure.

🔢 The Classic Counter Example

The clearest way to feel a closure is a counter. The outer function defines a count variable and returns an inner function that increments it and gives it back.

closures.js
function makeCounter() {
let count = 0; // private variable
return function () {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Let’s read it from the top and see how count survives between calls:

  • let count = 0 creates a variable that lives inside makeCounter.
  • The returned inner function does count = count + 1 and return count, so it changes and reads the same count from the outer function.
  • const counter = makeCounter() runs the outer function once and saves the inner function in counter.
  • Each counter() call adds one to that same count and returns the new value, so we get 1, then 2, then 3.

The count is never reset, because the inner function remembers the one variable instead of making a new one. Nothing outside makeCounter can read or change count directly, which is exactly the kind of privacy we want.

🔒 Each Closure Keeps Its Own Copy

Every time you call the outer function, it builds a brand new variable and a brand new closure around it. Two counters do not share their counts.

closures.js
const counterA = makeCounter();
const counterB = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 → counterB has its own private count

Let’s trace why the two counters don’t interfere:

  • makeCounter() runs twice, so it creates two separate count variables, one for each call.
  • counterA closes over the first count, and counterB closes over the second count.
  • Calling counterA() twice raises its own count to 1 then 2.
  • Calling counterB() uses a different remembered count that started fresh at 0, so it returns 1.

Because each closure remembers its own variable, the counters count independently. This is what makes closures so useful for private state.

Private by default

A variable inside the outer function cannot be touched from the outside. The only way to change it is through the inner function you returned. That is data privacy with no extra tools.

🏭 Function Factories

Because each call to the outer function produces a fresh closure, you can use a function to build customized functions. This pattern is called a function factory.

closures.js
function makeMultiplier(factor) {
return function (value) {
return value * factor; // `factor` is remembered
};
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

Let’s see how each returned function remembers its own factor:

  • makeMultiplier(factor) is the outer function, and factor is the value it closes over.
  • The inner function takes a value and returns value * factor, reaching back to the factor it was created with.
  • const double = makeMultiplier(2) returns an inner function that remembers factor is 2.
  • const triple = makeMultiplier(3) returns a separate inner function that remembers factor is 3.
  • So double(5) gives 10 and triple(5) gives 15, each using its own remembered factor.

You wrote the logic once and stamped out two ready-made tools.

⚠️ Common Mistakes to Avoid

Mistake Problem Solution
Expecting the variable to reset count is not zeroed on each call; it keeps its last value Remember the closure holds the same variable between calls
Thinking closures share state Each call to the outer function makes its own private copy Call the outer function once if you want one shared counter
Overusing closures Wrapping everything in functions makes code hard to read Use them only when you need private or remembered state

Here is the reset mistake in code so the difference is clear.

closures.js
// ❌ A new counter every time means it always returns 1
console.log(makeCounter()()); // 1
console.log(makeCounter()()); // 1
// ✅ Keep one counter so the count grows
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

Let’s compare the two halves line by line:

  • makeCounter()() calls the outer function and then immediately calls the returned inner function. Each makeCounter() builds a brand new count at 0, so it always returns 1.
  • In the second half, const counter = makeCounter() runs the outer function once and keeps the returned inner function.
  • Calling that same counter() twice reuses one remembered count, so it grows from 1 to 2.

The fix is to hold on to a single counter instead of making a new closure on every line.

🔧 Try It Yourself!

  1. Write a makeCounter function and call it once to get a counter, then call that counter three times.
  2. Create two separate counters and confirm they count independently.
  3. Write a makeMultiplier factory and build a double and a triple function from it.
  4. Write a function that remembers a setting, such as makeGreeting("Sam"), and call the returned function.

🧩 What You’ve Learned

  • ✅ A closure is a function that remembers variables from where it was created
  • ✅ An inner function can use the outer function’s variables, even after the outer function finishes
  • ✅ The counter example shows a private count that survives between calls
  • ✅ Each call to the outer function makes its own private copy
  • ✅ Closures power data privacy and function factories

🚀 What’s Next?

Closures depend on which variables a function can see. Next we will study those rules directly. Let’s continue to Scope.

Share & Connect