Skip to content
beginner

JavaScript Closures: How Functions Remember Variables

A function can outlive the variables it was born next to. That sentence sounds wrong until you run it.

Published 2026-10-03Updated 2026-10-046 min read
A lizard peeking through a crevice in a tree trunk with rough textured bark.
A lizard peeking through a crevice in a tree trunk with rough textured bark. Photo by Helmut Retsch on Pexels.

A function can outlive the variables it was born next to. That sentence sounds wrong until you run it.

Here is the smallest version of the puzzle. Read it, then predict what prints before you scroll.

function makeGreeting() {
  const name = "Ada";

  function greet() {
    console.log("Hello, " + name);
  }

  return greet;
}

const sayHello = makeGreeting();
sayHello();
Hello, Ada

Look at the order of events. makeGreeting() runs, declares name, declares greet, and returns greet. Then the outer call is finished. By the rule most beginners carry around — "local variables disappear when the function ends" — name should be gone by the time sayHello() runs.

It isn't. The function still reads it.

That is a closure in JavaScript: a function bundled with the environment where it was written. Not a copy of the values. A live connection to the place those variables live.

What a Closure Actually Captures

When JavaScript creates a function, it links that function to the lexical environment where it was written. Plain English: the set of names the function could see at that spot in the source code, plus a link to the environment around it.

You already met this idea when you learned scope. A function can read variables declared outside it. Closures are the part of that rule that keeps working after the outer function returns.

The correction that matters: the function keeps a reference, not a snapshot. If the outer variable changes later, the function sees the new value, because it is looking at the same variable, not a photocopy of it.

Common mistake: Assuming the closure froze the value at creation time. It didn't. It holds the variable itself.

Knowledge check

Check your understanding

Answer this question before you continue.

After an outer variable changes, what does a closure that reads it use?
Misconception Check

Focus: Distinguish a closure's live access to an outer variable from a snapshot of its value.

Trace a Closure Step by Step

The classic example is a counter. Run it and watch the number climb across separate calls.

function makeCounter() {
  let count = 0;

  return function () {
    count = count + 1;
    return count;
  };
}

const counter = makeCounter();
console.log(counter());
console.log(counter());
console.log(counter());
1
2
3

Three separate calls, one shared count. The outer function ran only once, so only one count was ever created — and the returned function keeps reaching back into it.

Here is the tracing checklist I use when a closure confuses me:

  1. Find where the function was declared. Not where it is called.
  2. List the outer names it actually uses. Those are the bindings it reaches back to.
  3. Follow the calls. Each call reuses the same captured environment unless a new outer call created a fresh one.

That third point is the one people miss. A closure is decided by where the function is written, not where it is invoked. You can pass counter into another function, store it in an array, or hand it to a timer — it still reads its original environment.

Knowledge check

Check your understanding

Answer this question before you continue.

What does this code print, in order?
Output Prediction

Focus: Trace repeated calls to one closure that increments its captured counter.

function makeCounter() {
  let count = 0;
  return function () {
    count = count + 1;
    return count;
  };
}
const counter = makeCounter();
console.log(counter());
console.log(counter());

Two Counters, Two Separate Memories

Two parallel paths show counterA connected to its own environment with count 2, and counterB connected to a separate environment with count 1.
Each call to makeCounter creates a separate count that its returned function can update.

Now call the outer function twice.

const counterA = makeCounter();
const counterB = makeCounter();

console.log(counterA());
console.log(counterA());
console.log(counterB());
1
2
1

counterB starts at 1 even though counterA is already at 2. Each call to makeCounter() created a separate lexical environment with its own count. The returned function from the first call retains access to the first environment; the returned function from the second call retains access to the second. Two calls, two environments, two independent memories.

This is the mechanism behind private state with closures. The count variable is not a property on the returned function. It is not reachable from outside at all. The only code that can touch it is the code that was written inside makeCounter.

Knowledge check

Check your understanding

Answer this question before you continue.

What does this code print, in order?
Output Prediction

Focus: Predict how separate calls to a closure factory create independent state.

const counterA = makeCounter();
const counterB = makeCounter();
console.log(counterA());
console.log(counterA());
console.log(counterB());

Keeping State Private

You can share one private variable across several functions by returning an object of methods.

function createWallet() {
  let balance = 0;

  return {
    add(amount) {
      balance = balance + amount;
    },
    spend(amount) {
      balance = balance - amount;
    },
    getBalance() {
      return balance;
    },
  };
}

const wallet = createWallet();
wallet.add(50);
wallet.spend(20);
console.log(wallet.getBalance());
console.log(wallet.balance);
30
undefined

add, spend, and getBalance all close over the same balance. They can read and change it. From outside, wallet.balance is undefined — there is no such property. The state lives in the closed-over environment, and the only doors into it are the methods you chose to expose.

That is data hiding without classes, and it is one of the most practical reasons closures exist.

Knowledge check

Check your understanding

Answer this question before you continue.

In the wallet example, what does `wallet.balance` evaluate to after `wallet.add(50)`?
Output Prediction

Focus: Recognize that a closed-over variable is not automatically a property of the returned object.

Where Closures Show Up in Real Code

You have probably already used closures without naming them.

  • Event handlers. A click handler that reads a variable from the surrounding setup code is closing over it.
  • Callbacks. When you pass a function to setTimeout or an array method, it remembers the variables where it was defined.
  • Function factories. A makeDiscount(rate) that returns a pricing function is a closure holding onto rate.

In every case, the pattern works for the same reason: the function carries its environment with it, so setup code can finish and the behavior still holds.

When to Reach for a Closure, and When Not To

Use a closure when a small piece of state should stay hidden and only a few functions should touch it. That is the sweet spot.

Skip it when a plain object, a function parameter, or a module-level variable is clearer. If nothing needs hiding, a closure just adds indirection.

Warning: The most common beginner surprise is expecting a snapshot. If a loop variable changes after a function is created, the function sees the final value, not the value at creation time. When that bites you, the fix is usually to create a new environment per iteration.

Your Next Step

Write one outer function that returns two functions sharing a single private counter — say, increment and reset. Before you run it, write down what you expect each call to print. Then run it and compare.

When that matches, move on to closures inside callbacks and event handlers. That is where the concept stops being an exercise and starts being the reason your code works.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

A callback is passed to a timer after setup code finishes. Why can it still read a variable from that setup code?
Question 1 of 2Single Choice

Focus: Explain why a callback can use variables from the code where it was defined after setup has finished.

A small value should stay hidden, and only two functions should read or update it. Which approach best matches the article's guidance?
Question 2 of 2Single Choice

Focus: Choose a closure when a small piece of state should remain hidden behind a few functions.

References

  1. Closures - JavaScript | MDNdeveloper.mozilla.org
Practical resource

Want a more structured JavaScript path?

Use the JavaScript for AI Applications Starter Pack to turn individual tutorials into a focused path from language fundamentals to interactive AI applications.

View the bundle
Coming soon

JavaScript for AI Applications Starter Pack

Build the JavaScript foundation behind modern interactive applications. The JavaScript for AI Applications Starter Pack takes you from core language concepts through functions, browser events, forms, DOM updates, application state, debugging, and practical projects—so you can learn how interfaces take input, work with structured data, respond to users, and turn results into experiences people can actually use.

$9
PDF BundleJavaScriptWeb DevelopmentAI Applications
  • 253-page illustrated PDF
  • 12 guided JavaScript chapters
  • Visual concept diagrams
  • Self-assessment quizzes
  • Bonus deep-dive sections
  • DOM, events, forms & application state
  • Browser projects, debugging & practical workflows

Coming soon

Keep learning

Related tutorials

Continue with nearby JavaScript topics and beginner-friendly explanations.