Lesson 9 of 18 · 0 of 2 exercises solved

Closures & Scope

You will learn how decides where a can be used, and how a lets a keep access to variables from where it was created. , event handlers and many common patterns rely on this.

JavaScript has function scope (var), block scope (let/const), and scope. Understanding which scope a variable lives in is critical to avoiding subtle with callbacks, , and timers.

// Scope basics
let x = 1          // module scope

function outer() {
  let y = 2        // function scope

  function inner() {
    let z = 3      // function scope of inner
    console.log(x, y, z)  // can see all three
  }

  inner()
  // console.log(z)  // ReferenceError — z doesn't exist here
}

// Block scope with let/const
{
  let blockVar = "inside"
  const blockConst = "also inside"
}
// console.log(blockVar)  // ReferenceError

// Classic closure
function makeCounter(start = 0) {
  let count = start  // this variable lives in the closure

  return {
    increment() { count++; return count },
    decrement() { count--; return count },
    value()     { return count },
    reset()     { count = start },
  }
}

const counter = makeCounter(10)
console.log(counter.increment())  // 11
console.log(counter.increment())  // 12
console.log(counter.decrement())  // 11
console.log(counter.value())      // 11

// Two counters — separate closures, separate state
const c1 = makeCounter()
const c2 = makeCounter(100)
c1.increment()
console.log(c1.value(), c2.value())  // 1, 100
JavaScript sandboxCtrl+Enter runs
// The classic loop closure bug
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// Prints: 3 3 3 — all callbacks share the same var i

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

// Fix 2: IIFE to capture current value (old-school)
for (var i = 0; i < 3; i++) {
  ((captured) => {
    setTimeout(() => console.log(captured), 0)
  })(i)
}

// Practical closure: memoization
function memoize(fn) {
  const cache = new Map()
  return function(...args) {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)
    const result = fn.apply(this, args)
    cache.set(key, result)
    return result
  }
}

const expensiveFib = memoize(function fib(n) {
  if (n <= 1) return n
  return expensiveFib(n - 1) + expensiveFib(n - 2)
})

console.log(expensiveFib(40))  // fast!

// Partial application via closure
const multiply = (a) => (b) => a * b
const double = multiply(2)
const triple = multiply(3)
console.log(double(5))   // 10
console.log(triple(5))   // 15
JavaScript sandboxCtrl+Enter runs

Common question

Does a variable disappear when its function returns?

Scope is the part of the code where a name is visible: global, function or block. A closure is a function together with the scope it was created in; when you call it later, it can still read and write those variables. That is what gives JavaScript private state without classes, callbacks that remember their context, and the module pattern.

Prefer let and const so each block and each loop iteration gets its own bindings. When a callback needs to remember something, let it close over a variable instead of using a global. And if several closures share a variable by accident, expect them to see each other's changes.

Source: The ECMAScript specification gives every function a to the environment it was created in. Closures are not an add-on; they are how JavaScript functions work.

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • Local variables live in the function's frame and vanish when it returns.

  • A closure captures the value a variable had when the closure was created.

  • Block scope and function scope are the same thing.

Practice

Write makeCounter() that returns a function; each call to that function returns the next number, starting at 1. The count must be private, and two counters must not share state.

Exercise

A private counter

3 checks
function makeCounter() {
  // ...
}

const a = makeCounter()
const b = makeCounter()
console.log(a(), a(), b())
 
JavaScript sandboxCtrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

A classic bug: every timeout logs 3 because var is function-scoped. Fix the loop so logged ends up as [0, 1, 2]. One keyword change is enough.

Exercise

The loop closure bug

2 checks
const logged = []

for (var i = 0; i < 3; i++) {
  setTimeout(() => logged.push(i), 10)
}

await new Promise(r => setTimeout(r, 50))
console.log(logged)
 
JavaScript sandboxCtrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Check yourself

Quick check

What is a closure?

Finished reading?2 exercises still open. You can come back to them later.

Tip: use the ← and → keys to move between lessons.