Skip to main content
Knowledge Hub

Closures in JavaScript

Understanding how functions remember their lexical scope

Last updated: March 25, 2025

Closures are one of the most powerful and advanced concepts in JavaScript. They are at the heart of data privacy, encapsulation, callbacks, and functional programming.

A closure is a function that remembers the variables from its outer scope, even after that outer function has finished executing. In other words, a closure gives inner functions access to the outer function’s variables, even after the outer function is gone from the call stack.

The Core Idea

function outer() {
  let name = "John";

  function inner() {
    console.log("Hello " + name);
  }

  return inner;
}

const greet = outer();  // outer() executed, inner() returned
greet();                // "Hello John"

What’s happening?

  1. outer() executes and creates a function execution context with local variable name
  2. It returns the inner function, not calling it yet
  3. When we call greet() (which is actually inner()), the outer function’s context is gone from the call stack
  4. But the inner function still remembers the variable name via its closure. It holds a reference to outer’s lexical environment

That memory of variables is what we call a closure.

Formal Definition

A closure is the combination of a function and its lexical environment within which it was declared.

When a function is defined, JavaScript captures its surrounding scope (the lexical environment), not the call-time environment.

How Closures Work Internally

A Lexical Environment is made up of:

{
  Environment Record: { local variables },
  Outer Environment Reference: pointer to parent Lexical Environment
}

When the JavaScript engine defines a function, it stores the outer environment reference of where that function was created. When executed, that reference allows the function to walk up the scope chain to find variables.

Step-by-Step Example

function outer() {
  let counter = 0;

  function inner() {
    counter++;
    console.log(counter);
  }

  return inner;
}

const fn = outer();
fn();  // 1
fn();  // 2

Flow

  1. outer() runs and creates local variable counter = 0
  2. inner() is defined and its closure closes over counter
  3. outer() returns inner
  4. Normally, counter would be destroyed when outer() finishes, but since inner() still references it, the memory stays alive
  5. Each call to fn() increments the same counter

The function fn has a persistent private state.

Visualization

Global Execution Context

├── outer() Execution Context
│     ├── counter = 0
│     └── inner() function → closure created

└── inner() Execution Context (later)
      └── Accesses counter from outer()'s lexical environment

Closures Preserve References, Not Copies

A closure doesn’t copy values. It preserves references.

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

const c1 = makeCounter();
const c2 = makeCounter();

console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 1

Each call to makeCounter() creates a new lexical environment. So c1 and c2 have independent closures.

Closure in Loops Example

Common interview question:

for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

Output:

4
4
4

Why? var is function-scoped, not block-scoped. All closures refer to the same i, which becomes 4 after the loop.

Fix with let:

for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

Fix with IIFE:

for (var i = 1; i <= 3; i++) {
  (function(i) {
    setTimeout(() => console.log(i), 1000);
  })(i);
}

Both create a new lexical environment for each iteration.

Data Privacy with Closures

Closures allow private variables, a key feature in functional design.

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
      console.log("Deposited:", amount);
    },
    getBalance() {
      console.log("Balance:", balance);
    }
  };
}

const acc = createBankAccount(100);
acc.deposit(50);
acc.getBalance(); // 150
console.log(acc.balance); // undefined

balance is private and only accessible through the closure functions.

Real-World Applications

Use CaseDescription
Data privacy/encapsulationProtect variables from outside access
Callbacks & event handlersMaintain state across async operations
Functional programmingCurrying, partial application
MemoizationStore previously computed results
Module patternCreate isolated module-level state

Advanced Example: Function Factories

function makeMultiplier(multiplier) {
  return function(num) {
    return num * multiplier;
  };
}

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

console.log(double(5)); // 10
console.log(triple(5)); // 15

Each returned function has its own closure with a different multiplier.

Closures and Asynchronous Code

Closures are especially important in async operations like setTimeout:

function delayedPrint(msg, delay) {
  setTimeout(() => console.log(msg), delay);
}
delayedPrint("Hello!", 2000);

Even after delayedPrint() finishes executing, the inner arrow function still remembers msg thanks to the closure.

Summary

ConceptDescription
ClosureFunction plus surrounding lexical scope
PurposeAllow inner functions to access outer variables
LifetimePersists even after outer function returns
Used ForData privacy, async callbacks, functional patterns

Closure Flow

Function declared → Closure formed (captures outer variables)

Function returned → Outer scope removed from stack

Closure retains reference to those outer variables

Inner function still can read/write them later

Memory Behavior

JavaScript engines use garbage collection, but closures prevent garbage collection from deleting variables that are still referenced. Be careful with large closures inside event handlers. They can cause memory leaks if not managed properly.

A closure is formed when an inner function remembers variables from its lexical scope, even after that scope has finished executing. Closures are essential for data privacy, maintaining state in async operations, and building flexible functional patterns.