Ad – 728Γ—90
βš™οΈ Functions

JavaScript Closures – Functions That Remember

A closure is a function bundled together with its lexical environment β€” the variables from the scope where it was defined. Closures are one of JavaScript's most powerful features and the foundation of private state, factory functions, the module pattern, memoization, and much more.

⏱️ 22 min read 🎯 Beginner πŸ“… Updated 2026

What Is a Closure?

A closure forms when an inner function references a variable from its outer function's scope, and that inner function survives (is returned, stored, or passed) beyond the outer function's execution.

JavaScript
function makeCounter() {
  let count = 0; // This variable "lives on" in the closure

  return function() {
    count++;       // Accesses the outer 'count'
    return count;
  };
}

const counter = makeCounter(); // makeCounter's execution is DONE
// But 'count' is still alive because 'counter' closes over it

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// Each call to makeCounter creates a NEW closure with its own count
const counter2 = makeCounter();
console.log(counter2()); // 1 β€” independent from counter
β–Ά Output
1 2 3 1
ℹ️
The Lexical Environment

Every function in JavaScript carries a hidden reference to its lexical environment β€” the scope where it was created. When the function is called later (even after the outer function has returned), it still has access to that environment. This is a closure.

Data Privacy and Encapsulation

Closures let you create "private" variables that cannot be accessed directly from outside β€” only through the returned interface.

JavaScript
function createBankAccount(initialBalance) {
  let balance = initialBalance; // Private β€” inaccessible from outside

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error("Amount must be positive");
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
console.log(account.deposit(50));      // 150
console.log(account.withdraw(30));     // 120
console.log(account.getBalance());     // 120

// Cannot access balance directly
console.log(account.balance); // undefined β€” it's private!
β–Ά Output
150 120 120 undefined

Factory Functions with Closures

JavaScript
function createLogger(prefix) {
  let logCount = 0;

  return {
    log(message) {
      logCount++;
      console.log(`[${prefix}] #${logCount}: ${message}`);
    },
    getCount() { return logCount; }
  };
}

const appLogger  = createLogger("APP");
const authLogger = createLogger("AUTH");

appLogger.log("Server started");
appLogger.log("Listening on port 3000");
authLogger.log("User logged in");
authLogger.log("Session created");
appLogger.log("Request received");

console.log("App logs:", appLogger.getCount());   // 3
console.log("Auth logs:", authLogger.getCount());  // 2
β–Ά Output
[APP] #1: Server started [APP] #2: Listening on port 3000 [AUTH] #1: User logged in [AUTH] #2: Session created [APP] #3: Request received App logs: 3 Auth logs: 2

Module Pattern

The module pattern uses an IIFE + closure to create a module with private state and a public API β€” the precursor to ES6 modules.

JavaScript
const cartModule = (function() {
  // Private state
  const items = [];
  let total = 0;

  // Private function
  function recalculate() {
    total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  }

  // Public API
  return {
    addItem(item) {
      items.push(item);
      recalculate();
    },
    removeItem(name) {
      const idx = items.findIndex(i => i.name === name);
      if (idx !== -1) { items.splice(idx, 1); recalculate(); }
    },
    getTotal() { return total; },
    getItems() { return [...items]; } // Return copy, not reference
  };
})();

cartModule.addItem({ name: "Book", price: 15, qty: 2 });
cartModule.addItem({ name: "Pen", price: 3, qty: 5 });
console.log("Total: $" + cartModule.getTotal()); // Total: $45
β–Ά Output
Total: $45

Memoization with Closures

Memoization caches function results β€” the cache is stored in a closure variable.

JavaScript
function memoize(fn) {
  const cache = {}; // Closed over by the returned function

  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      console.log("Cache hit for:", key);
      return cache[key];
    }
    console.log("Computing for:", key);
    cache[key] = fn(...args);
    return cache[key];
  };
}

const slowSquare = memoize(n => {
  // Simulate slow computation
  return n * n;
});

console.log(slowSquare(5));  // Computing, then 25
console.log(slowSquare(5));  // Cache hit, then 25
console.log(slowSquare(6));  // Computing, then 36
β–Ά Output
Computing for: [5] 25 Cache hit for: [5] 25 Computing for: [6] 36

Partial Application

JavaScript
function multiply(a, b) {
  return a * b;
}

// Partial application β€” fix the first argument
function partial(fn, firstArg) {
  return function(secondArg) {
    return fn(firstArg, secondArg); // firstArg is closed over
  };
}

const double = partial(multiply, 2);
const triple = partial(multiply, 3);

console.log(double(5));  // 10
console.log(triple(5));  // 15
console.log(double(10)); // 20
β–Ά Output
10 15 20

Event Handlers with State

JavaScript
// Simulating a button click tracker (browser pattern)
function createClickTracker(buttonId) {
  let clicks = 0;

  function handleClick() {
    clicks++;
    console.log(`Button ${buttonId} clicked ${clicks} time(s)`);
  }

  // In a browser: document.getElementById(buttonId).addEventListener('click', handleClick);
  // Here we simulate:
  return handleClick;
}

const btn1Handler = createClickTracker("btn-1");
const btn2Handler = createClickTracker("btn-2");

btn1Handler(); // Button btn-1 clicked 1 time(s)
btn1Handler(); // Button btn-1 clicked 2 time(s)
btn2Handler(); // Button btn-2 clicked 1 time(s) β€” independent
β–Ά Output
Button btn-1 clicked 1 time(s) Button btn-1 clicked 2 time(s) Button btn-2 clicked 1 time(s)

The Classic Loop Closure Bug

JavaScript
// BUG: var creates one shared 'i' across all iterations
const printFuncsVar = [];
for (var i = 0; i < 3; i++) {
  printFuncsVar.push(() => console.log("var i:", i));
}
printFuncsVar[0](); // var i: 3 β€” not 0!
printFuncsVar[1](); // var i: 3
printFuncsVar[2](); // var i: 3

// FIX 1: Use let β€” each iteration gets a NEW binding
const printFuncsLet = [];
for (let j = 0; j < 3; j++) {
  printFuncsLet.push(() => console.log("let j:", j));
}
printFuncsLet[0](); // let j: 0
printFuncsLet[1](); // let j: 1
printFuncsLet[2](); // let j: 2

// FIX 2: IIFE to capture the value (older workaround)
const printFuncsIIFE = [];
for (var k = 0; k < 3; k++) {
  printFuncsIIFE.push((function(captured) {
    return () => console.log("IIFE k:", captured);
  })(k));
}
printFuncsIIFE[0](); // IIFE k: 0
β–Ά Output
var i: 3 var i: 3 var i: 3 let j: 0 let j: 1 let j: 2 IIFE k: 0

Memory Considerations

Closures keep the outer scope's variables alive as long as the closure exists. This is intentional but can cause memory leaks if closures are not properly released.

⚠️
Closure Memory Leaks

If a closure captures a reference to a large object (e.g., a DOM node or a big array), that object stays in memory as long as the closure exists. Set variables to null when done, or remove event listeners to allow garbage collection.

Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Create a createRateLimiter(maxCalls, windowMs) function using a closure that:

  • Tracks how many times the returned function has been called within the time window
  • Allows the call if under the limit
  • Blocks with "Rate limit exceeded" otherwise
  • Resets the counter after windowMs milliseconds

The counter must be private (closure variable).

πŸ”₯ Challenge Exercise

Implement an observable(initialValue) function using closures that:

  • Stores a private value
  • Returns an object with get(), set(newVal), and subscribe(fn)
  • When set is called, notifies all subscribed functions with (newVal, oldVal)
  • Returns an unsubscribe function from subscribe

πŸ“‹ Summary

  • A closure = a function + its lexical environment (the variables from its defining scope).
  • Inner functions close over outer variables, keeping them alive even after the outer function returns.
  • Use closures for: private state, factory functions, module pattern, memoization, partial application.
  • The classic var loop bug is fixed by using let (block-scoped per iteration).
  • Be mindful of memory: closures prevent garbage collection of captured variables.

Interview Questions

  • What is a closure in JavaScript? Provide a definition and example.
  • How do closures enable data privacy/encapsulation?
  • Explain the classic for-loop closure bug with var and how let fixes it.
  • What is the module pattern and how does it use closures?
  • Can closures cause memory leaks? How do you prevent them?

Frequently Asked Questions

Are all functions closures in JavaScript?+

Technically yes β€” every JavaScript function closes over its surrounding scope. But we typically say a function "is a closure" when it actually makes use of variables from an outer scope that has already returned, making the concept meaningful and visible.

What is the difference between a closure and a class?+

Both provide private state and a public interface. Classes use prototype-based inheritance and the class keyword. Closures are simpler (functions + variables) and don't require this or new. Closures are better for functional style; classes for object-oriented patterns with inheritance.

Do closures work with const variables?+

Yes. Closures capture the variable binding (reference), not the value. With const, you can't reassign the binding, but if the const holds an object or array, you can still mutate its contents via the closure.