JavaScript Closures, Scope, and the Module Pattern: The Complete Guide

JavaScript Closures, Scope, and the Module Pattern: The Complete Guide
10 views

Closures are mentioned in every JavaScript interview but rarely understood deeply. A closure is a function that retains access to its outer scope even after the outer function has returned. This seemingly simple definition has profound implications for private state, module patterns, event handlers, and the classic loop variable bug that catches developers repeatedly. This guide focuses on JavaScript closures, with the production trade-offs senior engineers need to make sound decisions.

⚡ TL;DR: Closure = function + reference to its outer lexical scope. Variables are captured by reference, not by value. The loop bug happens because var is function-scoped; fix with let or IIFE. Use closures for private state and factory functions. ES modules are the modern module pattern.

Lexical scope — how JavaScript looks up variables — JavaScript closures

// JavaScript uses lexical (static) scope — determined at write time, not run time
const x = 'global';
function outer() {
  const x = 'outer';
  function inner() {
    const x = 'inner';
    console.log(x); // 'inner' — finds x in own scope first
  }
  inner();
  console.log(x); // 'outer' — inner scope not accessible
}

// Scope chain: inner → outer → global → undefined
// Each function creates a new scope

Closures in depth — captured by reference

function makeAdder(x) {
  return function(y) { return x + y; }; // Closes over x
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
add5(3);  // 8 — x is 5 in add5's closure
add10(3); // 13 — x is 10 in add10's closure

// Captured BY REFERENCE — mutation is visible:
function makeCounter() {
  let count = 0;
  return {
    inc: () => ++count,
    dec: () => --count,
    val: () => count
  };
}
const c = makeCounter();
c.inc(); c.inc(); c.val(); // 2 — same count variable, three closures over it

The classic loop bug

// BUG: var is function-scoped, all closures share ONE i
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3 (not 0, 1, 2)
}
// When setTimeout fires, loop is done, i = 3

// FIX 1: let (block-scoped, new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// FIX 2: IIFE (captures value at time of call)
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 100))(i);
}

Module pattern — private state with closures

// IIFE module: creates private scope
const UserStore = (() => {
  let users = [];              // Private: inaccessible from outside
  let nextId = 1;

  return {                     // Public API
    add(name) {
      users.push({ id: nextId++, name });
    },
    getAll() {
      return [...users];       // Return copy, prevent external mutation
    },
    count() { return users.length; }
  };
})();

UserStore.add('Alice');
UserStore.getAll(); // [{ id: 1, name: 'Alice' }]
UserStore.users;    // undefined — private!

// ES Modules (modern approach — same concept, better syntax):
// users.js
let users = [];  // Module-scoped = effectively private
export function addUser(name) { users.push(name); }
export function getUsers() { return [...users]; }
  • ✅ Use let/const in loops — never var inside closures
  • ✅ Closures for private state — factory functions and module pattern
  • ✅ ES modules for modern code — native support, tree-shakeable
  • ✅ Return copies of arrays/objects from closures to prevent mutation
  • ❌ Never rely on closure variables being immutable — they are mutable references
  • ❌ Never create closures inside tight loops unnecessarily — memory overhead

Closures are central to the JavaScript interview guide — the loop bug appears in 80% of senior JS interviews. External reference: MDN Closures documentation.

Recommended Reading

→ Designing Data-Intensive Applications — The essential book every senior developer needs.

→ The Pragmatic Programmer — Timeless engineering wisdom for writing better code.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Don’t Miss the Next Cheat Code

Join 1,000+ senior developers getting expert JS, Python, AWS and system design secrets weekly.

✓ No spam✓ Unsubscribe anytime

Related: TypeScript for JavaScript Developers: The Migration Guide That Actually Works · JavaScript Promises: The Complete Guide From Basics to Advanced Patterns · JavaScript Async/Await: Every Pattern, Pitfall, and Performance Trick

Related: JavaScript Array Methods: Every Method Explained With Real Examples · Design Patterns in JavaScript: The Gang of Four Patterns Every Senior Dev Uses · Webpack and Vite: Modern JavaScript Bundling Explained for Application Developers

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.