March 26, 20265 min read

JavaScript Closures Actually Explained

What closures really are, why they exist, and how to use them — factory functions, data privacy, the classic loop bug, and memory leak gotchas.

javascript closures functions scope fundamentals
Ad 336x280

Most explanations of closures make them sound more complicated than they are. A closure is just a function that remembers the variables from the place where it was defined, even after that outer function has finished running. That's it. The function carries a backpack of variables with it.

Here's the simplest possible closure:

function makeGreeter(name) {
  return function () {
    console.log(Hey, ${name});
  };
}

const greetSam = makeGreeter('Sam');
greetSam(); // "Hey, Sam"

makeGreeter has already returned by the time we call greetSam(). But the inner function still has access to name. It closed over that variable. That's a closure.

Why does JavaScript even do this?

Because functions are first-class values. You can pass them around, return them, store them in variables. If a function couldn't remember its surrounding variables, returning functions from other functions would be nearly useless. The language would lose half its expressiveness.

Every function in JavaScript creates a closure. Most of the time you don't notice because the function runs in the same scope where it was defined. Closures become visible when a function escapes its original scope — gets returned, passed as a callback, or stored somewhere.

Practical use: data privacy

JavaScript doesn't have private fields in the way Java does (well, #privateField exists now in classes, but closures came first). The classic pattern:

function createCounter() {
  let count = 0;

return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; },
};
}

const counter = createCounter();
counter.increment();
counter.increment();
counter.getCount(); // 2
// counter.count — undefined. Can't touch it directly.

count is completely inaccessible from the outside. The only way to interact with it is through the methods that closed over it. This was the module pattern before ES modules existed, and it's still useful.

Factory functions

Closures make factory functions trivial:

function createMultiplier(factor) {
  return (number) => number * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

double(5); // 10
triple(5); // 15

Each returned function remembers its own factor. You can create as many as you want, and they don't interfere with each other.

Partial application

Same idea, slightly different use. You fix some arguments upfront:

function fetchFromAPI(baseURL) {
  return function (endpoint) {
    return fetch(${baseURL}${endpoint}).then(r => r.json());
  };
}

const api = fetchFromAPI('https://api.example.com');
api('/users'); // fetches https://api.example.com/users
api('/products'); // fetches https://api.example.com/products

You configure once, use many times. This pattern shows up everywhere in real codebases.

The classic loop bug

This one has tripped up every JavaScript developer at least once:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 — not 0, 1, 2

All three callbacks close over the same i variable. By the time the timeouts fire, the loop has finished and i is 3. Every closure sees the same value because var is function-scoped, not block-scoped.

The fix with let:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2
let creates a new binding for each loop iteration. Each callback closes over its own copy. This is probably the single best reason let was added to the language.

Before let existed, the workaround was an IIFE:

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

Ugly, but it worked. Be glad we have let now.

Event handlers

Closures are how you attach context to event handlers without globals:

function setupButton(buttonId, message) {
  const btn = document.getElementById(buttonId);
  btn.addEventListener('click', () => {
    alert(message);
  });
}

setupButton('btn-hello', 'Hello!');
setupButton('btn-bye', 'Goodbye!');

Each handler remembers its own message. No global variables, no data attributes, no this juggling.

Memory leak gotchas

Closures keep their outer variables alive. That's the whole point. But it means if you're not careful, you can hold onto large objects longer than you intended:

function processData() {
  const hugeArray = new Array(1_000_000).fill('data');

return function summarize() {
return hugeArray.length; // hugeArray stays in memory forever
};
}

const getSummary = processData();
// hugeArray is still in memory because summarize references it

If summarize only needs the length, extract it first:

function processData() {
  const hugeArray = new Array(1_000_000).fill('data');
  const length = hugeArray.length;

return function summarize() {
return length; // hugeArray can now be garbage collected
};
}

This matters most with DOM references, large data structures, and long-lived closures like event handlers. If your app's memory usage keeps climbing, check what your closures are holding onto.

Quick mental model

When you see a function defined inside another function, ask: "Does this inner function use any variable from the outer function?" If yes, that's a closure doing real work. The inner function will keep those variables alive as long as it exists.

That's genuinely all there is to it. The concept is simple — the power comes from applying it in the right places.

Practice writing closures hands-on at CodeUp — the interactive exercises make the concept click faster than reading about it.

Ad 728x90