Skip to content

JavaScript Closures

This lesson explains JavaScript Closures in JavaScript with beginner-friendly examples, practical use cases, and clear best practices.

JavaScript Closures Overview

A JavaScript closure is created when an inner function remembers and accesses variables from its outer function even after the outer function has finished executing. Closures are one of the most powerful features of JavaScript and are widely used in callbacks, event handlers, modules, private variables, memoization, and asynchronous programming.

Every time a function is created, JavaScript forms a closure around its surrounding lexical environment. Understanding closures is essential for writing modern JavaScript applications using React, Angular, Vue, Node.js, and other frameworks.

Concept Description Common Usage
Closure Inner function remembers outer variables. Private state.
Lexical Scope Functions access variables from parent scopes. Nested functions.
Private Variables Hide internal data. Encapsulation.
Callback Functions Maintain surrounding state. Events and timers.
Module Pattern Create reusable private modules. Large applications.

JavaScript Closure Example

function createCounter() {

  let count = 0;

  return function() {
    count++;
    return count;
  };

}

const counter = createCounter();

console.log(counter());
console.log(counter());
console.log(counter());

The inner function remembers the count variable even after createCounter() finishes execution. This behavior is known as a closure.

Top 10 JavaScript Closure Examples

Closures are frequently used in modern JavaScript applications for state management, callbacks, encapsulation, and reusable utility functions.

# Example Syntax
1 Counter Function function counter() { let c = 0; return function() { return ++c; }; }
2 Private Variable function user() { let name = "John"; return function() { return name; }; }
3 Greeting Function function greet(msg) { return function(name) { return msg + name; }; }
4 Tax Calculator function tax(rate) { return function(price) { return price * rate; }; }
5 Event Handler button.onclick = function() { console.log("Clicked"); };
6 setTimeout Callback setTimeout(function() { console.log("Done"); }, 1000);
7 Memoization function cache() { const data = {}; return function() { }; }
8 Module Pattern function module() { return { }; }
9 Array Callback numbers.map(function(n) { return n * 2; });
10 Configuration Factory function config(env) { return function() { return env; }; }

Top 10 Tricky Closure Examples (with Fix)

These are common closure edge cases that confuse many developers. Each row shows a tricky pattern and the safer approach.

# Tricky Scenario Problem Pattern Fix Pattern Expected Result
1 var in loop with async callback for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0) for (let i = 0; i < 3; i++) ... 0,1,2 instead of 3,3,3
2 Shared closure state across calls const fn = makeCounter(); fn(); fn(); used globally const a = makeCounter(); const b = makeCounter(); Independent counters per instance
3 Capturing mutable object reference config.theme = "dark"; return () => config.theme const theme = config.theme; return () => theme Stable snapshot value in closure
4 Closure inside forEach uses outer reassigned variable let role; users.forEach(u => { role = u.role; handlers.push(() => role); }) handlers.push(() => u.role) Each handler returns correct user role
5 Event listeners in loop capture wrong index for (var i = 0; i < btns.length; i++) btns[i].onclick = () => console.log(i) for (let i = 0; i < btns.length; i++) ... Correct button index on click
6 Memory leak via large closed-over data const big = hugeArray; return () => big.length return () => hugeArray.length; hugeArray = null when unused Lower retained memory
7 Stale closure in async function let token = getToken(); setTimeout(() => callApi(token), 1000) setTimeout(() => callApi(getToken()), 1000) Uses latest token at execution time
8 Closure with accidental global mutation count = count + 1 (missing declaration in factory) let count = 0; return () => ++count State remains private and predictable
9 Memoization key collision cache[arg] = result for object args const key = JSON.stringify(arg) or Map Correct cache hits/misses
10 Debounce closure loses this return function() { clearTimeout(t); t = setTimeout(fn, d); } t = setTimeout(() => fn.apply(this, args), d) Correct context and arguments preserved

Best Practices

  • Use closures to create private variables.
  • Keep closure functions small and focused.
  • Avoid storing unnecessary large objects inside closures.
  • Use closures for reusable factory functions.
  • Use closures with callbacks and event handlers.
  • Document complex closure logic for better maintainability.
  • Release unused references to improve memory usage.
  • Use modern module patterns when organizing applications.

Common Mistakes

  • Confusing closures with scope.
  • Keeping unnecessary variables alive inside closures.
  • Creating memory leaks by retaining unused objects.
  • Overusing nested functions.
  • Using closures when a simple function is sufficient.
  • Not understanding lexical scope.
  • Writing overly complex closure logic.

Key Takeaways

  • A closure remembers variables from its outer scope.
  • Closures work because of JavaScript's lexical scope.
  • They are commonly used for private variables and callbacks.
  • Closures power many JavaScript design patterns.
  • Modern frameworks rely heavily on closures.
  • Understanding closures is essential for advanced JavaScript development.

Pro Tip

If you understand lexical scope, you already understand most of how closures work. A closure simply allows a function to remember variables from the scope in which it was created, even after that scope has finished executing.