JavaScript Closures Deep Dive: Complete Guide

A closure is a function that remembers the variables from the scope where it was created, even after that scope is gone. Learn how closures work, why they matter, and what you can build with them.

7 min read

A closure is a function combined with the variables from the scope where it was created. The function "remembers" those variables even after the outer scope has finished executing. Closures are not a special syntax or a keyword. They are the natural result of two JavaScript features working together: lexical scope and functions as first-class values.

Here is the simplest closure:

javascriptjavascript
function createGreeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}!`; // greeting is from the outer scope
  };
}
 
const sayHello = createGreeter("Hello");
console.log(sayHello("Alex")); // "Hello, Alex!"

createGreeter runs, receives "Hello", and returns the inner function. Then createGreeter finishes, and its local variables should be gone.

But sayHello still remembers a greeting of "Hello". That remembered connection is a closure.

Why Closures Work

Closures work because of JavaScript's scope chain. When a function is created, it stores a reference to the scope it was born in. Even when that scope is no longer active, the reference keeps the variables alive:

How a closure preserves variables

The outer scope would normally be garbage collected. But because the inner function still references it, the variables persist. Each call to the outer function creates a fresh scope and a fresh closure over it.

The Counter Example: Why Each Closure Is Independent

A closure is created fresh on every call to the outer function, not once for the whole program. This counter factory makes that independence visible:

javascriptjavascript
function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

Creating two separate counters and interleaving calls to each one shows that they never interfere with each other, since each one closes over its own private count variable:

javascriptjavascript
const counterA = createCounter();
const counterB = createCounter();
 
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 (independent!)
console.log(counterA()); // 3

Every call to createCounter creates a NEW count variable and a NEW closure. The two counters do not share state. This is the key reason closures are useful for data privacy, each closure has its own private variables that no other code can touch.

Data Privacy with Closures

Closures are JavaScript's primary mechanism for private state without classes. Each method below closes over the same private balance variable, and that variable has no name anywhere outside this function:

javascriptjavascript
function createBankAccount(initialBalance) {
  let balance = initialBalance; // Private variable
 
  return {
    deposit(amount) { balance += amount; return balance; },
    withdraw(amount) {
      if (amount > balance) return "Insufficient funds";
      balance -= amount;
      return balance;
    },
    getBalance() { return balance; },
  };
}

Using the account only through its methods keeps every read and write funneled through the same controlled surface, since balance itself is never exposed as a property:

javascriptjavascript
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50));   // 150
console.log(account.withdraw(30));  // 120
console.log(account.balance);       // undefined -- cannot access directly

balance is completely private. The only way to read or change it is through the returned methods. This pattern, an outer function that returns an object of methods that close over private state, is called the module pattern.

Closures in Loops: The Classic Trap

javascriptjavascript
// Bug: all callbacks share the same i
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3
 
// Fix with let: each iteration gets its own i
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2

With var, there is one i for the whole function. All three callbacks close over the same i, which is 3 by the time they run. With let, each loop iteration creates a new i, so each callback closes over its own value.

If you must use var, the old fix was an IIFE to capture each value:

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

Closures and the Event Loop

Closures are what make asynchronous callbacks useful. When you pass a callback to setTimeout or addEventListener, the callback remembers the scope where it was created:

javascriptjavascript
function setupButton(buttonId, message) {
  const button = document.getElementById(buttonId);
 
  button.addEventListener("click", function() {
    // This callback closes over message
    alert(message);
  });
}
 
setupButton("btn1", "You clicked button 1!");
setupButton("btn2", "You clicked button 2!");

Each call to setupButton creates a closure that remembers its own message. The event listener runs long after setupButton has returned, but message is still available.

Practical: Once Function

A closure can track whether a function has been called, using a private flag that only the returned function can see:

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

Calling the wrapped function repeatedly only ever runs the original logic on the first attempt, since every call after that finds the flag already set and returns nothing:

javascriptjavascript
const initialize = once(() => console.log("Initialized!"));
initialize(); // "Initialized!"
initialize(); // (nothing)
initialize(); // (nothing)

The returned function closes over called. After the first invocation, called is true and all subsequent calls are ignored.

Practical: Memoization

Closures can cache function results, storing each computed answer the first time and reusing it on every later call with the same input:

javascriptjavascript
function memoize(fn) {
  const cache = {};
  return function(arg) {
    if (arg in cache) {
      return cache[arg];
    }
    const result = fn(arg);
    cache[arg] = result;
    return result;
  };
}

Calling the memoized function twice with the same argument only computes the result once, since the second call finds the answer already sitting in the cache:

javascriptjavascript
const memoizedSquare = memoize((n) => {
  console.log("Computing...");
  return n * n;
});
 
console.log(memoizedSquare(5)); // Computing... 25
console.log(memoizedSquare(5)); // 25 (from cache, no Computing...)

The cache object is private to the closure. No external code can access or corrupt it.

For more closure patterns, see Practical Use Cases for JS Closures in Real Apps. For how to avoid memory issues, see How to Prevent Memory Leaks in JavaScript Closures.

Rune AI

Rune AI

Key Insights

  • A closure is created when a function accesses variables from an outer scope and survives beyond that scope.
  • Closures are the mechanism behind data privacy, function factories, and module patterns.
  • Each call to a factory function creates a new closure with independent state.
  • Closures keep outer variables alive. Release references when you are done to avoid memory leaks.
  • Closures are not a special feature. They are the natural result of lexical scope plus functions as values.
RunePowered by Rune AI

Frequently Asked Questions

Are closures unique to JavaScript?

No. Many languages support closures, including Python, Ruby, Swift, and Rust. However, closures are particularly central to JavaScript because of its heavy use of callbacks, event handlers, and functional patterns.

Do closures cause memory leaks?

They can if you hold references to closures you no longer need. A closure keeps its outer variables alive. If you create many closures in a loop or store them in a long-lived data structure without cleaning up, memory usage grows. Set references to null when done.

How do I know if I have created a closure?

Every function in JavaScript forms a closure over its surrounding scope. If a function uses variables from an outer scope, it has a meaningful closure. If it only uses its own parameters and local variables, the closure is technically present but empty.

Conclusion

A closure is a function plus its remembered environment. It is not a special syntax or keyword. It is the natural result of lexical scope combined with functions being first-class values. Closures give you data privacy, stateful functions, and the foundation for nearly every advanced JavaScript pattern. Once you internalize that a function carries its birth scope with it forever, closures stop being mysterious and start being useful.