Skip to content

JavaScript Closures Explained (With Interview Questions)

Closures in plain English: lexical scope, how closures work, common interview traps (loops + setTimeout), and practical use cases like memoization and data hiding.

Rajesh DhimanFebruary 08, 2026~14 min
On this page

A closure is one of those interview topics that sounds scary until you realize it’s just this:

A function “remembers” the variables from the place it was created.

30‑second interview answer

A closure is created when a function is defined inside another scope and retains a reference to its outer lexical environment. Even after the outer function finishes executing and pops off the call stack, the inner function can still access those variables because the JavaScript garbage collector does not free memory while reachable references remain.

Key points

  • Closures come from lexical scoping (where functions are written in source code), not async timing.
  • Closures hold live references to variables in the environment record, not immutable snapshots.
  • Used everywhere in production: private state, factories, memoization, event listeners, and React hooks.

1) The smallest closure example

function outer() {
  const name = 'Rajesh';
 
  function inner() {
    console.log(name);
  }
 
  return inner;
}
 
const fn = outer();
fn(); // 'Rajesh'

Even though outer() completed and its execution context popped off the stack, calling fn() still accesses name.

That persistent link to the outer environment record is the closure.


2) What closures are NOT

Closures are not:

  • a special syntax or keyword
  • exclusive to asynchronous code
  • exclusive to React hooks

Closures are an inherent feature of JavaScript because:

  • Lexical scoping determines identifier resolution at author time.
  • Functions are first-class citizens that can be passed as arguments, assigned to variables, and returned from other functions.

3) Classic interview question: “Why does this print 3, 3, 3?”

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

Why it happens:

  1. var is function-scoped (or globally scoped), not block-scoped.
  2. There is only one shared binding for i across all iterations.
  3. When the synchronous loop completes, i has incremented to 3.
  4. When the macrotask queue empties the timer callbacks, all 3 functions reference the identical binding i, logging 3, 3, 3.

Fix 1: Use let (Modern ES6)

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 2

Specification detail: In the ECMAScript spec, each iteration of a for loop with a let declaration creates a brand-new Lexical Environment Record, giving each callback its own closed-over copy of i.

Fix 2: IIFE (Immediately Invoked Function Expression)

for (var i = 0; i < 3; i++) {
  (function (x) {
    setTimeout(() => console.log(x), 0);
  })(i);
}
// Output: 0, 1, 2

The IIFE creates a new function execution context on each iteration, capturing the current value of i as the parameter x.


4) Production use cases (what senior interviewers look for)

A) Data hiding / encapsulation

function createCounter(initialValue = 0) {
  let count = initialValue; // Completely private
 
  return {
    increment() {
      count += 1;
      return count;
    },
    decrement() {
      count -= 1;
      return count;
    },
    getValue() {
      return count;
    },
  };
}
 
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.getValue());  // 11
console.log(counter.count);       // undefined (cannot be tampered with directly)

B) Memoization & Cache Isolation

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;
  };
}

5) Memory leaks and accidental retention

Because closures keep outer variables reachable, holding onto an inner function prevents garbage collection of everything in its scope chain.

The circular closure trap:

function attachHeavyListener() {
  const hugeData = new Uint8Array(10_000_000); // 10MB
 
  const element = document.getElementById('submit-btn');
  element.addEventListener('click', () => {
    // Only needs a small flag, but closes over hugeData's entire scope
    console.log('Button clicked');
  });
}

Fix: Nullify large objects or scope them outside the closure if the event listener remains active for the lifetime of the document.


6) Interactive interview practice

Question: Predict the output of this snippet:

function buildList() {
  const result = [];
  for (var i = 0; i < 3; i++) {
    result.push(function () {
      return i * 2;
    });
  }
  return result;
}
 
const list = buildList();
console.log(list[0]());
console.log(list[1]());
console.log(list[2]());

Output:

6
6
6

Step-by-step trace:

  1. var i creates a single variable bound to the buildList function scope.
  2. The loop runs 3 times, pushing 3 functions into result. None of them execute yet.
  3. The loop terminates when i reaches 3.
  4. buildList returns the array of functions.
  5. When list[0]() is called, it resolves i through its lexical scope. i is currently 3, so 3 * 2 = 6.
  6. list[1]() and list[2]() resolve the same i, returning 6 as well.

7) Rapid-fire interview Q&A

Q1: What is a closure in one sentence?

  • A function bundled together with references to its surrounding lexical environment.

Q2: Do closures copy values or reference them?

  • Closures keep live references to variable bindings in memory; modifying the variable will reflect when the closure reads it.

Q3: How do closures relate to React hooks?

  • Hooks like useState, useEffect, and useCallback rely on closures to preserve state between render passes. Stale closures occur when an effect or callback omits a state dependency, locking into an older render's variable.

Official specifications & references


Written by Rajesh Dhiman at Eunix Tech for the Frontend Interview preparation track.

Companion Book

Looking for structured, offline interview prep?

Free online lessons • Portable Amazon Kindle / Paperback

JavaScript Interview Guide cover
Recommended for Core JS Rounds

JavaScript Interview Guide

By Rajesh Dhiman

Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.

View on Amazon ↗Kindle & Paperback
Frontend Interview Guide (2025 Edition) cover
Recommended for React & Frontend Rounds

Frontend Interview Guide (2025 Edition)

By Rajesh Dhiman

Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.

View on Amazon ↗Kindle & Paperback

Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.

RD

Written by Rajesh Dhiman

Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.