Skip to editor content
learningjavascript.orglesson 9 of 25

Closures and Scope

Closures are one of the most important concepts in JavaScript. They allow functions to remember and access variables from the place where they were defined, even after that outer function has finished executing. Before we can understand closures, we need to understand scope, the set of rules that determines where variables are visible in your code.

What You'll Learn

  • How JavaScript determines where variables are accessible (scope)
  • The difference between global, function, and block scope
  • What closures are and how they work
  • Immediately Invoked Function Expressions (IIFE)
  • Practical closure patterns including the module pattern

Understanding Scope

Scope is the area of your program where a variable is visible. JavaScript has three types of scope: global, function, and block scope.

// Global scope - visible everywhere
const globalMessage = "I am global";

function outerFunction() {
  // Function scope - visible only inside outerFunction
  const outerMessage = "I am outer";

  if (true) {
    // Block scope - visible only inside this if block
    const blockMessage = "I am block-scoped";
    let blockLet = "Also block-scoped";
    var functionVar = "I am function-scoped (var ignores blocks)";

    console.log(globalMessage);
    console.log(outerMessage);
    console.log(blockMessage);
  }

  console.log(functionVar); // var leaks out of blocks
  // console.log(blockMessage); // Would cause ReferenceError

  console.log(globalMessage);
  console.log(outerMessage);
}

outerFunction();
console.log(globalMessage);
// console.log(outerMessage); // Would cause ReferenceError

The key takeaway: let and const are block-scoped, while var is function-scoped. This is a major reason to prefer let and const in modern JavaScript.

Lexical Scope

JavaScript uses lexical scope (also called static scope), which means the scope of a variable is determined by where it is written in the source code, not where it is called from. Inner functions can access variables from their outer functions:

function createGreeting(greeting) {
  // greeting is in the outer function's scope

  function greetPerson(name) {
    // This inner function can access greeting from the outer scope
    console.log(greeting + ", " + name + "!");
  }

  greetPerson("Alice");
  greetPerson("Bob");
}

createGreeting("Hello");
createGreeting("Bonjour");

// Nested scopes work at any depth
function level1() {
  const a = "level 1";

  function level2() {
    const b = "level 2";

    function level3() {
      const c = "level 3";
      console.log(a, b, c); // Can access all outer scopes
    }

    level3();
  }

  level2();
}

level1();

What Is a Closure?

A closure is a function that retains access to its outer scope even after the outer function has returned. This is what makes closures special: the inner function "closes over" the variables it needs.

function createCounter() {
  let count = 0; // This variable is "enclosed" by the returned function

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

const counter = createCounter();

// createCounter has finished executing, but the inner function
// still has access to count
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

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

The variable count is not accessible from outside, yet the returned function can still read and modify it. This is the essence of closures.

Predict

This factory returns TWO functions that close over the same count, and it's called twice. Predict both logged values before running.

function makeCounter() {
let count = 0;
return {
  inc() { count++; return count; },
  get() { return count; },
};
}

const c = makeCounter();
c.inc();
c.inc();
console.log(c.get()); // line A

const d = makeCounter(); // a second, separate call
console.log(d.get());    // line B

Two things to hold onto from this: a closure captures its variable by reference (so inc and get see each other's changes to the same count), and each call to the factory makes a fresh, independent closure. The next sections lean on both — and the loop pitfall later is exactly what happens when you expect independent captures but get a shared one.

Closures for Data Privacy

One of the most practical uses of closures is creating private variables. There is no private keyword in JavaScript, but closures give us true data privacy:

function createPerson(name, age) {
  // These variables are private - no outside access
  let _name = name;
  let _age = age;

  return {
    getName() {
      return _name;
    },
    getAge() {
      return _age;
    },
    setAge(newAge) {
      if (typeof newAge === "number" && newAge > 0 && newAge < 150) {
        _age = newAge;
        console.log(_name + "'s age updated to " + _age);
      } else {
        console.log("Invalid age");
      }
    },
    greet() {
      console.log("Hi, I'm " + _name + " and I'm " + _age + " years old");
    }
  };
}

const person = createPerson("Alice", 30);
person.greet();
person.setAge(31);
person.greet();
person.setAge(-5);

// Cannot access _name or _age directly
console.log(person._name);  // undefined
console.log(person._age);   // undefined
console.log(person.getName()); // "Alice"

Immediately Invoked Function Expressions (IIFE)

An IIFE is a function that runs immediately after it is defined. Before let and const existed, IIFEs were the primary way to create private scope and avoid polluting the global namespace:

// Basic IIFE syntax
const result = (function() {
  const secret = "hidden value";
  return secret.toUpperCase();
})();

console.log(result);
// console.log(secret); // Would cause ReferenceError

// IIFE with parameters
const greeting = (function(name) {
  return "Hello, " + name + "!";
})("World");

console.log(greeting);

// IIFE for one-time initialization
const config = (function() {
  const defaults = { theme: "light", lang: "en" };
  const overrides = { theme: "dark" };

  // Merge and return - computation happens once
  const merged = {};
  for (let key in defaults) merged[key] = defaults[key];
  for (let key in overrides) merged[key] = overrides[key];

  return Object.freeze(merged);
})();

console.log(config);
console.log(config.theme);

The Module Pattern

The module pattern combines closures and IIFEs to create self-contained units of code with public and private parts. This was the go-to pattern before ES modules:

const ShoppingCart = (function() {
  // Private state
  let items = [];

  // Private helper function
  function calculateTotal() {
    return items.reduce(function(sum, item) {
      return sum + item.price * item.quantity;
    }, 0);
  }

  // Public API
  return {
    addItem(name, price, quantity) {
      quantity = quantity || 1;
      items.push({ name, price, quantity });
      console.log("Added " + quantity + "x " + name);
    },

    removeItem(name) {
      items = items.filter(function(item) { return item.name !== name; });
      console.log("Removed " + name);
    },

    getTotal() {
      return "$" + calculateTotal().toFixed(2);
    },

    listItems() {
      if (items.length === 0) {
        console.log("Cart is empty");
        return;
      }
      items.forEach(function(item) {
        console.log("  " + item.name + " x" + item.quantity + " - $" + (item.price * item.quantity).toFixed(2));
      });
      console.log("  Total: " + this.getTotal());
    }
  };
})();

ShoppingCart.addItem("Coffee", 4.99, 2);
ShoppingCart.addItem("Sandwich", 8.50);
ShoppingCart.addItem("Cookie", 2.25, 3);
ShoppingCart.listItems();
ShoppingCart.removeItem("Sandwich");
ShoppingCart.listItems();

// Private variables are not accessible
console.log(ShoppingCart.items); // undefined

Recall

Without scrolling up: the module pattern returns an object of methods that share private state through a closure. In 06-objects-and-prototypes you built objects with methods too — the player object kept its data in plain public properties like this.score. What does the closure give the module pattern that a public property does not?

Common Closure Pitfalls

Closures with loops cause the single most common closure bug in JavaScript. It comes straight from the two rules the Predict above just made: a closure captures its variable by reference, and var is function-scoped (not block-scoped), so a whole loop shares one variable. Diagnose it before reading the fix:

Debug

This loop builds three handler functions, one per iteration. The author expected them to return 'handler 0', 'handler 1', 'handler 2'. They don't. Predict what all three actually return, then fix it so each returns its own number.

const assert = require('assert');

const handlers = [];

for (var i = 0; i < 3; i++) {
handlers.push(function () {
  return "handler " + i;
});
}

const first = handlers[0]();
const second = handlers[1]();
const third = handlers[2]();

assert.strictEqual(first, "handler 0", 'expected handler 0, got ' + first);
assert.strictEqual(second, "handler 1", 'expected handler 1, got ' + second);
assert.strictEqual(third, "handler 2", 'expected handler 2, got ' + third);

console.log(first);
console.log(second);
console.log(third);

Expected output: handler 0 handler 1 handler 2

The bug is var. Because var is function-scoped, the whole loop shares one i; all three closures capture that same variable by reference, and by the time they run, the loop has finished and left i at 3 — so every call returns handler 3. Switching to let gives each iteration its own block-scoped i, so the closures capture three different variables and return handler 0, handler 1, handler 2. That one-word difference is a core reason to prefer let/const.

Try It Yourself

Reading about closures is not the same as building with them. This is a build task: a small program that reports its own pass/fail. Three factory functions are stubbed out — each one is supposed to close over some private state and hand back a way to use it. Run it as-is and it fails immediately, telling you which factory is still missing. Implement each one until every check passes and it prints All checks passed.

The three factories reuse exactly what this lesson taught: a private variable enclosed by the returned function or object, captured by reference so successive calls see each other's changes, and fresh and independent on every call to the factory. The starter has the stubs and the checks; you write only the logic inside each factory. Nothing above spells out all three answers, so you will have to assemble them yourself.

Build

Finish the build. Three factory functions are stubbed out and the checks below them fail until each one closes over the right private state and returns the right value. Run it as-is to see which check fails first, decide what that factory is missing, then implement the three factories until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

const assert = require('assert');

// TODO 1: makeAccumulator() returns a FUNCTION that remembers a running total.
//   Each call adds its argument to the running total and returns the new total.
//   const add = makeAccumulator();
//   add(10) -> 10, then add(5) -> 15, then add(-3) -> 12
function makeAccumulator() {
// your code here
}

// TODO 2: makeTagger(prefix) returns a FUNCTION that closes over prefix and a
//   private counter, handing out sequential labels one call at a time.
//   const tag = makeTagger("task");
//   tag() -> "task-1", then tag() -> "task-2", then tag() -> "task-3"
function makeTagger(prefix) {
// your code here
}

// TODO 3: createWallet(starting) returns an OBJECT whose balance is private.
//   deposit adds and returns the new balance; withdraw refuses short funds by
//   returning false and leaving the balance untouched; balance() reads it.
//   const w = createWallet(100);
//   w.deposit(50) -> 150, w.withdraw(30) -> 120, w.withdraw(999) -> false, w.balance() -> 120
function createWallet(starting) {
// your code here
}

// --- Build checks: these must all pass. Do not edit below this line. ---
const add = makeAccumulator();
assert.strictEqual(typeof add, "function", "makeAccumulator() should return a function you can keep calling");
assert.strictEqual(add(10), 10, "the accumulator should return 10 after adding 10 to a fresh total of 0");
assert.strictEqual(add(5), 15, "the same accumulator should remember 10 and return 15 after adding 5");
assert.strictEqual(add(-3), 12, "the same accumulator should return 12 after adding -3 to 15");

const other = makeAccumulator();
assert.strictEqual(other(100), 100, "a second accumulator should start fresh at 0, independent of the first");

const tag = makeTagger("task");
assert.strictEqual(typeof tag, "function", "makeTagger('task') should return a function you can keep calling");
assert.strictEqual(tag(), "task-1", "the tagger should return 'task-1' on its first call");
assert.strictEqual(tag(), "task-2", "the same tagger should return 'task-2' on its second call");
assert.strictEqual(tag(), "task-3", "the same tagger should return 'task-3' on its third call");

const wallet = createWallet(100);
assert.strictEqual(typeof wallet, "object", "createWallet(100) should return an object of methods");
assert.strictEqual(wallet.deposit(50), 150, "deposit(50) on a wallet of 100 should return the new balance 150");
assert.strictEqual(wallet.withdraw(30), 120, "withdraw(30) should return the new balance 120");
assert.strictEqual(wallet.withdraw(999), false, "withdraw(999) should refuse (return false) when funds are short");
assert.strictEqual(wallet.balance(), 120, "balance() should still read 120 after the refused withdrawal");
assert.strictEqual(wallet.starting, undefined, "the private balance must not be reachable as a property");

console.log("All checks passed.");
console.log("Running total after +10, +5, -3:", add(0));
console.log("Next two tags:", tag(), tag());
console.log("Wallet balance:", wallet.balance());

Expected output: All checks passed. Running total after +10, +5, -3: 12 Next two tags: task-4 task-5 Wallet balance: 120

Once it passes, try two variations and predict each before running:

  1. Read the counter before bumping it. In makeTagger, change the body to return prefix + "-" + counter++ (post-increment: read, then add) instead of bumping first, then predict the first tag. The first call returns task-0, so the assert fires — 'task-0' !== 'task-1' — because post-increment hands back the counter's old value before the private state advances. An instructive assert failure about when the closed-over counter changes.
  2. Ask the accumulator a different question. The final log calls add(0) to peek at the running total without changing it. Change that to add(10) and predict the echoed line. Because the accumulator's private total is captured by reference and survives between calls, it was already 12, so this adds 10 and echoes 22 — the closure remembering its state across every call. This changes the echoed output, not any check.

Capstone milestone

Milestone — the Task Manager service. The capstone's service owns a private task list behind a closure and exposes add/toggle/remove/getTasks as a public API — the module pattern, exactly like the wallet you just built and the ShoppingCart. Confirm you can build a closure-backed service.

Hint: You don't need the full capstone yet — this confirms the private-state-behind-a-closure skill the task-manager service is built on. There, createTaskManager() closes over a private tasks array and exposes add/toggle/remove, exactly like createWallet in the build task above.

  • Wrote a factory function that keeps state private in a closed-over variable (not a returned property)
  • Returned an object of methods that all share and mutate that same private state
  • Confirmed the private state is unreachable from outside — reading it as a property gives undefined
  • Confirmed two instances from the factory have independent, non-shared state

Key Takeaways

  • Scope determines where variables are accessible: global, function, or block
  • let and const are block-scoped; var is function-scoped
  • Lexical scope means inner functions can access outer variables based on where they are written
  • A closure is a function that retains access to its outer scope after the outer function has returned
  • Closures enable data privacy by keeping variables inaccessible from the outside
  • IIFEs run immediately and create an isolated scope
  • The module pattern combines IIFEs and closures to expose a public API while hiding private state

Next Steps

With closures and scope under your belt, you are ready to tackle asynchronous programming. The next lesson covers callbacks, Promises, and async/await, which all rely heavily on closures to manage data across time.

Pro Tip: If you ever struggle to understand a piece of JavaScript code, ask yourself: "Where was this function defined, and what variables could it see at that point?" That question unlocks most closure-related mysteries.

Next lesson

Async and Promises

Master asynchronous JavaScript with callbacks, Promises, and async/await. Learn to write non-blocking code and handle async errors.

26 min