LoopLens

JavaScript closures, explained

A closure is a function together with the variables from the place where it was created. The function keeps access to those variables even after the outer function has returned. Every JavaScript function is a closure; it only becomes visible when a function is used outside the place it was written.

Try it

This counter remembers its count between calls. Press Next step and watch the Scope and variables panel: when next() runs, it still sees makeCounter's count, although makeCounter has finished.

Test yourself. When this is on, LoopLens pauses before every step that prints to the console and asks what it will print. Pick an answer to check it, or choose Just show me.Step 0 of 27 · virtual clock 0 ms
Event loop:Script (now)MicrotasksTask, then it repeats.The event loop runs the whole script first. Then it runs every microtask, then one task, and repeats.

Code (current line highlighted)

1current line: function makeCounter() {
2 let count = 0;
3 return function () {
4 count = count + 1;
5 return count;
6 };
7}
8
9const next = makeCounter();
10console.log(next());
11console.log(next());
12
13const other = makeCounter();
14console.log(other());

Call stack (top first)The function running right now is on top. Calling a function adds a frame; returning removes it. Only one frame runs at a time.

main script

Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.

empty
The whole script runs as ONE task on the call stack. Lines run top to bottom.

Microtask queue (next first)Promise callbacks (.then, code after await, queueMicrotask) wait here. Once the call stack is empty, all of them run before the next task.

empty

Task queue (next first)Timer callbacks wait here. The event loop takes one task, runs it, then empties the microtask queue before taking the next.

empty

Console output

nothing yet

Scope and variables (yellow = changed this step)

no variables yet

Timeline (click a block or use the arrow keys)Script, function calls and returnsawait and microtasksTimers and tasksConsole outputErrors

Step by step: what the example does

function makeCounter() {
  let count = 0;
  return function () {
    count = count + 1;
    return count;
  };
}

const next = makeCounter();
console.log(next());
console.log(next());

const other = makeCounter();
console.log(other());
  1. makeCounter() runs and creates a local variable count = 0.
  2. It returns the inner function without calling it. That function was created inside makeCounter, so it keeps a link to makeCounter's variables.
  3. makeCounter's frame leaves the call stack, but count is not thrown away: the returned function still refers to it.
  4. Each next() call adds 1 to that same count, so the output is 1, then 2.
  5. makeCounter() is called again for other. This creates a brand-new count, so other() prints 1. The two counters never share a variable.

Lexical scope: where a function is written matters

JavaScript decides which variables a function can see from where the function is written in the code, not from where it is called. This is called lexical scope. When a function looks up a name, it checks its own variables first, then the scope it was created in, then that scope's parent, up to the global scope. A closure is simply that chain of scopes staying available for as long as the function exists. The execution context page explains how these scopes are created.

Each call creates a new set of variables

Calling the outer function again doesn't reuse the old variables; it makes new ones. That is what makes closures useful for private state: nothing outside the counter can read or change count except through the function that closes over it.

function once(fn) {
  let done = false;
  return function (...args) {
    if (done) return;
    done = true;
    return fn(...args);
  };
}

const init = once(() => console.log("set up"));
init(); // set up
init(); // (nothing)

Here every function made by once gets its own done flag. Event handlers, timer callbacks and promise callbacks use closures in the same way to remember values from the code that created them.

The classic loop bug: var vs let

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var", i), 0);
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log("let", j), 0);
}

This prints var 3 three times, then let 0, let 1, let 2. A var variable belongs to the whole function (or script), so all three callbacks close over the same i, and by the time the timers run, the loop has already set it to 3. A let in a for loop gets a fresh copy for every round, so each callback remembers its own j. To step through it, choose var vs let in a loop in the example menu above.

Closures and memory

Variables that a closure uses stay in memory for as long as the closure can still be called. That is usually what you want. It only becomes a problem when a long-lived function, such as an event listener that is never removed, keeps large data alive by accident. Removing the listener, or not referring to the data inside it, lets the engine free it.

Common mistakes

  • Expecting a closure to copy a value. It keeps the variable itself, so it sees later changes, as in the var loop.
  • Thinking closures need special syntax. Any function that uses a variable from an outer scope is a closure.
  • Assuming two calls share state. Each call of the outer function makes its own variables, like the two counters above.

What LoopLens simulates

Scopes and closures are real in LoopLens: its interpreter creates a new scope for every call and keeps it alive while a function refers to it, exactly as described here. The Scope panel lists the scopes the current code can see, innermost first. The call stack, queues and clock are simulated.

Open the visualizer with your own code