Skip to Content
ConceptsClosures

Closures

A function that remembers variables from the scope where it was created — even after that scope is gone.

ELI5

You give a kid a backpack before they leave the house. The house is gone (the function finished), but the kid still has everything in the backpack. A closure is the backpack — it carries the variables with the function wherever it goes.

Why it matters

Without closures there’s no private state, no callbacks that remember context, and no React hooks. Every event handler, timer callback, and .then() relies on closures. Understand this and async code, hooks, and module patterns click into place.

When to use it

  • Private state that nothing outside can touch
  • Factories that return pre-configured functions
  • Callbacks that need to remember a value from when they were created
  • Memoization — caching results between calls
  • Counters, trackers, limiters without reaching for a class

1. Simple — counter

function makeCounter() { let count = 0 // closed-over variable — lives in the closure return function () { count++ // still has access even though makeCounter() already returned return count } } const counter = makeCounter() // makeCounter runs and exits, but count survives counter() // 1 — count is remembered counter() // 2 — same count, incremented again counter() // 3 — the closure keeps it alive between calls

2. Intermediate — private state

function createWallet(initial) { let balance = initial // private — only accessible through the returned methods return { // each method closes over the same `balance` variable deposit(amount) { balance += amount }, withdraw(amount) { balance -= amount }, getBalance() { return balance }, } } const wallet = createWallet(100) wallet.deposit(50) wallet.getBalance() // 150 — reads the closed-over balance wallet.balance // undefined — can't access it directly, it's private

3. Advanced — memoize

function memoize(fn) { const cache = new Map() // closed over — persists between every call to the returned function return function (...args) { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) // cache hit — closure remembered previous results const result = fn(...args) cache.set(key, result) // store in the closed-over cache for next time return result } } const fastSquare = memoize((n) => n * n) fastSquare(4) // 16 — computed and cached fastSquare(4) // 16 — returned from closure's cache, fn never runs again

This is the same pattern behind React’s useMemo.


Gotchas

The loop trap

// BROKEN — logs 3 three times for (var i = 0; i < 3; i++) { // all 3 callbacks close over the SAME `i` (var is function-scoped) setTimeout(() => console.log(i), 100) // 3, 3, 3 — i is already 3 when these run } // FIX — use let (block-scoped, creates a NEW binding per iteration) for (let i = 0; i < 3; i++) { // each callback gets its OWN `i` — a fresh closure per loop iteration setTimeout(() => console.log(i), 100) // 0, 1, 2 }

Mental model

ConceptWhat happens
Lexical scopeFunctions see variables from where they were defined, not where they’re called
ClosureA function + the variables it captured from its outer scope
LifetimeClosed-over variables live as long as the closure exists
Each call = new closureCalling a factory twice creates two independent closures
Last updated on