Skip to main content

Closures in JavaScript

A closure is a function that retains access to variables from the scope where it was created, even after that scope has finished executing.

Theory

TL;DR

  • Analogy: A closure is like a backpack. When a function is created, it packs the variables it needs from the surrounding scope and carries them along.
  • Core mechanic: JavaScript attaches an internal [[Scope]] reference to every function, pointing to the lexical environment where the function was defined. Variables referenced through this link stay alive as long as the function exists.
  • Every function is technically a closure. The real question is whether you are using this behavior on purpose.
  • Decision rule: Private state, factory functions, callbacks with context - closures. Three levels of nesting just to pass one value - reach for a class or plain object.

Quick example

javascript
function createCounter() { let count = 0; // This variable is "captured" return function increment() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 // count is NOT accessible out here - it is private

createCounter runs once and returns increment. The outer function is done. But count survives because increment still holds a reference to it. Every call to counter() reads and updates the same variable.

Key difference

Every function in JavaScript forms a closure over its lexical scope. What actually matters in interviews and in production is recognizing when you are intentionally using the captured scope. Writing setTimeout(() => doSomething(userId), 100) inside a React component? That callback closes over userId. Most developers write closures constantly without labeling them as such.

When to use

  • Private state: Variables you want to hide from the outside world - counters, config, internal caches
  • Factory functions: createMultiplier(2) bakes 2 into a new function without exposing it as a parameter on every call
  • Callbacks and event handlers: fetch, addEventListener, Promise chains - all capture variables from the surrounding context
  • Memoization and debounce: A timer ID or a cache object lives between calls, stored in a closure without polluting any outer scope

Avoid closures when a plain class or object literal would be easier to read. Three levels of nesting to pass a single value is a refactor signal, not a design pattern.

How JavaScript keeps variables alive

When a function is created, the V8 engine in Chrome and Node.js stores an internal [[Scope]] property on it. This property points to the lexical environment where the function was defined, including all variables in that scope and every parent scope above it.

The garbage collector keeps a variable alive as long as at least one reference to it exists. A closure holds that reference. Once you lose the last reference to the closure itself, the function and its captured variables become eligible for collection.

Common mistakes

Mistake 1: var in a loop

javascript
// Wrong - all callbacks see the same i for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // Logs 3, 3, 3 }, 1000); } // Right option 1 - let creates a new binding per iteration for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // Logs 0, 1, 2 }, 1000); } // Right option 2 - IIFE captures the value before the loop moves on (pre-ES6) for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); // Logs 0, 1, 2 }, 1000); })(i); }

With var, all three callbacks close over the same i. The loop finishes before any callback fires, so they all print 3. With let, each iteration creates a new binding. Three separate variables, three separate closures.

Mistake 2: Capturing large objects by accident

javascript
// Wrong - largeData stays in memory as long as the listener exists function setupListener() { const largeData = new Array(1000000).fill('data'); document.getElementById('btn').addEventListener('click', function() { console.log(largeData.length); // Captures the whole array }); } // Right - capture only the value you actually need function setupListener() { const largeData = new Array(1000000).fill('data'); const length = largeData.length; document.getElementById('btn').addEventListener('click', function() { console.log(length); // Captures a number, not the array }); // largeData is now eligible for garbage collection }

Mistake 3: this is not captured by a closure

javascript
// Wrong - this inside the callback is not obj const obj = { count: 0, increment: function() { setTimeout(function() { this.count++; // this is window or undefined in strict mode }, 1000); } }; // Right - arrow function inherits this from the enclosing scope const obj = { count: 0, increment: function() { setTimeout(() => { this.count++; // this is obj console.log(this.count); // 1 }, 1000); } };

Closures capture variables. this is not a variable in the usual sense - its value is determined by how the function is called, not where it was defined. Arrow functions have no this of their own and fall back to the enclosing scope's this, which is why they work here.

Mistake 4: Stale closure in React hooks

javascript
// Wrong - count is captured at mount and never updates inside the interval function Counter() { const [count, setCount] = React.useState(0); React.useEffect(() => { const timer = setInterval(() => { setCount(count + 1); // count is always 0 here }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; } // Right - use the updater form of setCount function Counter() { const [count, setCount] = React.useState(0); React.useEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); // prev is always the latest value }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }

The interval callback closes over count at the time the effect runs. With an empty dependency array that is once at mount, so count is 0 forever inside the callback. This is the stale closure issue that shows up most often in React-focused interviews - worth having a clean explanation ready. The updater function form bypasses it entirely: React passes the current state as an argument, so you never need to capture it.

Real-world usage

  • React hooks: useState, useCallback, and useRef are built on closures to persist values between renders
  • Redux middleware: Wraps dispatch in a closure to intercept actions before they reach the reducer
  • Express middleware: Route handlers close over the app instance, database connections, and configuration
  • Debounce and throttle: Store a timer ID between calls without touching any outer scope
  • Module pattern (pre-ES6): IIFEs created private variables before import/export existed
  • Dependency injection: createUserService(db) returns functions that close over the database connection

Follow-up questions

Q: Why doesn't the captured variable get garbage collected when the outer function returns?
A: The variable is still referenced by the returned function. The garbage collector keeps objects alive as long as any reference to them exists. Once you discard the closure, the variable is released too.

Q: Can a closure modify a captured variable, or only read it?
A: It can modify it. The closure holds a reference to the actual variable binding, not a snapshot. If two closures capture the same variable, they both see each other's writes.

Q: What is the difference between a closure and passing arguments?
A: Closures capture variables at creation time and retain them between calls. Arguments are passed fresh at each call. Use closures for state that needs to persist invisibly. Use arguments for dependencies that should be explicit at the call site.

Q: Can closures cause memory leaks? How do you prevent them?
A: Yes. If a closure captures a large object and the function is never collected (a DOM listener that is never removed, for example), the object stays in memory. Fix it by removing listeners when done, capturing only primitives or small values, and using WeakMap for closure-based caching.

Q: (Senior level) Explain why closures in setTimeout inside loops behave unexpectedly with var.
A: Closures capture variables by reference. When the timeout fires, the loop has already finished and i holds its final value. The callback reads that final value, not the value at the time the timeout was registered. With let, each iteration produces a separate variable in a new block scope, so each closure captures a different binding entirely.

Examples

Basic: factory function

javascript
function createMultiplier(multiplier) { // multiplier is captured in the closure return function(number) { return number * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 console.log(double(10)); // 20

Each call to createMultiplier produces a separate closure with its own multiplier. double and triple are independent. Calling one has no effect on the other.

Intermediate: memoization

javascript
function memoize(fn) { const cache = {}; // Private cache, shared across all calls to the returned fn return function(...args) { const key = JSON.stringify(args); if (key in cache) { return cache[key]; // Cache hit } const result = fn(...args); cache[key] = result; return result; }; } const heavyCalc = memoize((n) => { let sum = 0; for (let i = 0; i < n; i++) sum += i; return sum; }); console.log(heavyCalc(1000000)); // computed console.log(heavyCalc(1000000)); // returned from cache

cache is a private variable that persists across every call to the returned function. No global variable, no class. The closure is the only thing keeping cache alive and hidden.

Advanced: the loop trap and three ways to fix it

javascript
// Problem: var creates one shared binding for all event handlers const buttons = document.querySelectorAll('button'); for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', function() { console.log(i); // Always logs buttons.length, not 0, 1, 2 }); } // Fix 1: let - most readable, works in all modern environments for (let i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', function() { console.log(i); // Each callback captures its own i }); } // Fix 2: IIFE - captures the value before the loop advances (pre-ES6) for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', (function(index) { return function() { console.log(index); // index is a local copy of i at this iteration }; })(i)); }

The IIFE approach works because calling a function creates a new scope. At each iteration, i is passed as an argument and bound to index locally. The inner function closes over index, not i. With let, the engine handles this automatically. Same result, no extra syntax.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?