Back to all posts
·6 min read

Understanding JavaScript Closures

A deep dive into one of JavaScript's most powerful and often misunderstood features — closures. Learn how they work, why they matter, and how to use them effectively.

YB
Yogesh Bhardwaj
March 10, 2026

Understanding JavaScript Closures

Closures are one of those concepts in JavaScript that seem intimidating at first but become incredibly intuitive once you understand the mechanics behind them. They are fundamental to how JavaScript works and power many of the patterns we use daily — from event handlers to data privacy to functional programming.

What Is a Closure?

A closure is created when a function "remembers" the variables from its outer (enclosing) scope, even after the outer function has finished executing. In simpler terms, a closure gives you access to an outer function's scope from an inner function.

function createGreeting(greeting) {
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}

const sayHello = createGreeting("Hello");
console.log(sayHello("World")); // "Hello, World!"

Here, the inner function retains access to greeting even after createGreeting has returned. That's the closure in action.

Why Do Closures Matter?

Closures aren't just an academic concept. They're the foundation of several important patterns in JavaScript:

1. Data Privacy

JavaScript doesn't have built-in private variables, but closures let you emulate them:

function createCounter() {
  let count = 0;

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

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
// count is not directly accessible from outside

2. Function Factories

Closures let you create specialized functions from general ones:

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

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

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

3. Maintaining State in Async Code

Closures are essential when dealing with asynchronous operations:

function fetchUserData(userId) {
  const requestTime = Date.now();

  fetch(`/api/users/${userId}`).then((response) => {
    console.log(`Request for user ${userId} took ${Date.now() - requestTime}ms`);
    return response.json();
  });
}

The callback inside .then() closes over both userId and requestTime.

The Classic Loop Gotcha

One of the most common closure-related bugs in JavaScript involves loops:

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

This happens because var is function-scoped, and by the time the timeouts fire, i is already 3. The fix is straightforward 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 iteration, so each closure captures its own i.

Memory Considerations

Since closures retain references to their outer scope, they can prevent garbage collection of those variables. This is usually fine, but in performance-critical code, be mindful:

  • Avoid creating closures inside tight loops unnecessarily
  • Be cautious with closures that reference large data structures
  • Use closures intentionally, not accidentally

Wrapping Up

Closures are not magic — they are a natural consequence of how lexical scoping works in JavaScript. Once you internalize the idea that functions carry their birth environment with them, closures become a powerful tool rather than a source of confusion.

They enable patterns that make JavaScript expressive and flexible. Understanding them deeply is one of the most valuable investments you can make as a JavaScript developer.

JavaScriptFundamentalsProgramming