JavaScriptMedium

Explain closures in JavaScript

01

The Short Answer

A closure is a function that remembers the variables from its outer scope even after that outer function has finished executing. Every function in JavaScript forms a closure — it captures a reference to the variables in its surrounding lexical environment. This is what lets inner functions access outer variables long after the outer function has returned.

02

How Closures Form

When a function is defined inside another function, the inner function gets a permanent link to the outer function's variable environment. Even when the outer function finishes and its execution context is popped off the call stack, the variables it created stay alive in memory because the inner function still references them.

closure-basics.tstypescript
function createCounter() {
  let count = 0; // This variable lives on after createCounter returns

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

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

// count is not accessible from outside
// console.log(count); // ReferenceError: count is not defined

// Each call to createCounter creates a NEW closure with its own count
const counter2 = createCounter();
console.log(counter2()); // 1 (independent from counter)

The key insight: count isn't copied into the inner function — the inner function holds a reference to the same variable. That's why calling counter() multiple times increments the same count. And each call to createCounter() creates a completely independent closure with its own count.

03

Lexical Scoping — The Foundation

Closures work because JavaScript uses lexical scoping — a function's scope is determined by where it's written in the source code, not where it's called. When the engine encounters a variable, it walks up the scope chain from the current function outward until it finds a match. Closures are just this scope chain being preserved.

lexical-scope.tstypescript
const globalVar = 'global';

function outer() {
  const outerVar = 'outer';

  function middle() {
    const middleVar = 'middle';

    function inner() {
      // inner can access all variables in its lexical scope chain
      console.log(globalVar);  // 'global'
      console.log(outerVar);   // 'outer'
      console.log(middleVar);  // 'middle'
    }

    return inner;
  }

  return middle;
}

const middleFn = outer();  // outer() finishes, but outerVar lives on
const innerFn = middleFn(); // middle() finishes, but middleVar lives on
innerFn(); // Still has access to all three variables

Even though outer() and middle() have both returned, their local variables persist because innerFn still references them through the scope chain. The garbage collector can't clean them up while a closure holds a reference.

04

Practical Use Cases

Data Privacy / Encapsulation

Closures let you create truly private variables in JavaScript — variables that can only be accessed and modified through specific functions you expose. This is the module pattern's foundation and how you achieve encapsulation without classes.

private-state.tstypescript
function createBankAccount(initialBalance: number) {
  let balance = initialBalance; // Private — no direct access

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

const account = createBankAccount(100);
account.deposit(50);    // 150
account.withdraw(30);   // 120
account.getBalance();   // 120
// account.balance → undefined (truly private)

Function Factories

Closures let you create specialized functions from a general template. The outer function takes configuration, and the returned function uses that configuration every time it's called — without needing to pass it again.

function-factory.tstypescript
function createMultiplier(factor: number) {
  return (value: number) => value * factor;
}

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

double(5);     // 10
triple(5);     // 15
toPercent(0.75); // 75

Event Handlers and Callbacks

Every time you use a callback that references a variable from its surrounding scope, you're using a closure. This is extremely common in event handlers, timers, and async operations where the callback runs later but still needs access to variables from when it was created.

event-closure.tstypescript
function setupButton(buttonId: string, message: string) {
  const button = document.getElementById(buttonId);

  // This callback closes over 'message'
  button?.addEventListener('click', () => {
    alert(message); // message is remembered even though setupButton has returned
  });
}

setupButton('btn-hello', 'Hello!');
setupButton('btn-bye', 'Goodbye!');
// Each button has its own closure with its own message
05

The Classic Loop Trap

The most infamous closure gotcha involves loops with var. Because var is function-scoped (not block-scoped), all iterations share the same variable. By the time the callbacks run, the loop has finished and the variable holds its final value. This trips up developers who expect each callback to capture its own iteration value.

loop-trap.tstypescript
// ❌ The trap — all callbacks see i = 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 3, 3, 3 (not 0, 1, 2)
  }, 100);
}

// ✅ Fix 1: Use let (block-scoped — each iteration gets its own i)
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  }, 100);
}

// ✅ Fix 2: IIFE creates a new scope per iteration
for (var i = 0; i < 3; i++) {
  ((captured) => {
    setTimeout(() => {
      console.log(captured); // 0, 1, 2
    }, 100);
  })(i);
}

With let, each loop iteration creates a new block scope with its own copy of i. The closure in each setTimeout captures a different variable. With var, there's only one i shared across all iterations — every closure points to the same variable.

06

Memory Implications

Closures keep their referenced variables alive in memory. This is usually fine, but can cause memory leaks if you're not careful — especially with event listeners or long-lived callbacks that close over large objects you no longer need.

Avoiding closure memory leaks

  • Remove event listeners when components unmount
  • Clear intervals and timeouts when no longer needed
  • Set closed-over references to null when done with them
  • Be mindful of closures in long-lived caches or singleton patterns
07

Why Interviewers Ask This

Closures are fundamental to JavaScript — they power modules, callbacks, event handlers, React hooks, and most design patterns. Interviewers ask this to check if you understand lexical scoping, can explain why the loop trap happens, know practical applications (data privacy, factories, partial application), and understand the memory implications. A strong answer demonstrates both theoretical understanding and practical experience.

Quick Revision Cheat Sheet

Definition: A function that retains access to its outer scope's variables after the outer function returns

Mechanism: Lexical scoping — scope determined by source code position, not call site

Key insight: Closures capture references to variables, not copies of values

Loop trap: var is function-scoped — use let for per-iteration closures

Use cases: Data privacy, function factories, callbacks, partial application

Memory: Closed-over variables can't be GC'd while the closure exists