Skip to main content
Guides Skills and frameworks JavaScript Closures for Interviews — IIFEs, Scope Chains, and the Questions That Trip People
Skills and frameworks

JavaScript Closures for Interviews — IIFEs, Scope Chains, and the Questions That Trip People

9 min read · April 25, 2026

A JavaScript closures interview guide covering lexical scope, scope chains, IIFEs, loop traps, callbacks, stale closures, memory leaks, and how to answer common questions cleanly.

JavaScript Closures for Interviews — IIFEs, Scope Chains, and the Questions That Trip People

JavaScript closures for interviews are less about memorizing a definition and more about predicting what a function can still access after its outer function has returned. Interviewers use closures to test lexical scope, scope chains, IIFEs, var versus let, callbacks, encapsulation, stale state, and memory behavior. If you can trace which environment a function closes over, most “trick” questions stop being tricks.

JavaScript closures for interviews: the crisp definition

A closure is a function plus access to the lexical environment where that function was created. In plain English: an inner function can remember variables from its outer scope even after the outer function has finished executing.

Example:

function makeCounter() {
  let count = 0;
  return function increment() {
    count += 1;
    return count;
  };
}

const counter = makeCounter();
counter(); // 1
counter(); // 2

increment closes over count. The variable is not copied into the function as a frozen value. The closure keeps access to the variable binding. That distinction matters in loop questions and stale closure questions.

A strong interview definition: “A closure is created when a function references variables from an outer lexical scope. The function retains access to those bindings after the outer function returns, which enables private state, callbacks, factories, and partial application.”

Lexical scope and the scope chain

JavaScript uses lexical scope, meaning scope is determined by where code is written, not where a function is called. When a function tries to read a variable, JavaScript looks in the local scope first, then outer scopes, then the global scope. That chain of environments is the scope chain.

const taxRate = 0.08;

function createInvoice(subtotal) {
  const fee = 3;

  return function total() {
    return subtotal + fee + subtotal * taxRate;
  };
}

total can access subtotal, fee, and taxRate because of lexical scope. It does not matter where total is later called. Its outer environment was fixed when the function was created.

The interview trap: candidates say the function “remembers values.” More precise: it keeps references to bindings in its lexical environment. If the binding changes, the closure observes the change.

Closures are not only nested functions in examples

Closures show up everywhere in real JavaScript:

  • event handlers that access component state
  • callbacks passed to setTimeout, map, filter, or promises
  • module patterns with private variables
  • function factories and partial application
  • memoization caches
  • React hooks and stale closure bugs
  • Node middleware that captures configuration

Example with configuration:

function withApiKey(apiKey) {
  return function request(path) {
    return fetch(path, { headers: { Authorization: `Bearer ${apiKey}` } });
  };
}

The returned request function closes over apiKey. That is convenient, but it also means secrets or large objects can be retained longer than expected if closures live too long.

IIFEs: why old JavaScript used them

An IIFE is an immediately invoked function expression:

(function () {
  const hidden = "not global";
  console.log(hidden);
})();

Before let, const, and ES modules were common, IIFEs created private scope and avoided polluting the global object. They were also used to capture loop variables when var had function scope.

Classic example:

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

The IIFE receives the current i as j, creating a new binding for each iteration. Modern JavaScript usually uses let instead:

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

Because let is block-scoped, each loop iteration gets a fresh binding. In interviews, connect IIFEs to historical context. You do not need to use IIFEs everywhere now, but you should understand why they solved real scope problems.

The var loop question that trips people

The classic question:

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

This prints 3, 3, 3. Why? var is function-scoped, so all callbacks close over the same i binding. By the time the callbacks run, the loop has finished and i is 3.

Fixes:

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

or:

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

or the old IIFE pattern.

The key explanation: closures capture bindings, not snapshots of values. let creates a new binding per iteration in a for loop; var does not.

Private state and the module pattern

Closures can hide state without classes:

function createCart() {
  const items = [];

  return {
    add(item) { items.push(item); },
    count() { return items.length; },
    list() { return [...items]; }
  };
}

items is private because outside code cannot access it directly. The returned methods close over it. This is a common interview use case: encapsulation through closures.

But also mention tradeoffs. Private closure state can be harder to inspect, serialize, or reset in tests. Classes with private fields may be clearer in modern JavaScript. A senior answer does not claim closures are always better; it explains when they are useful.

Function factories and partial application

Closures are useful when you want to generate specialized functions:

const multiplyBy = factor => value => value * factor;
const double = multiplyBy(2);
const triple = multiplyBy(3);

double closes over factor = 2; triple closes over factor = 3.

Partial application works similarly:

function buildUrl(baseUrl) {
  return function pathUrl(path) {
    return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
  };
}

This pattern appears in API clients, middleware, validation, logging, and dependency injection. It lets you bind stable configuration once and reuse it without passing the same argument everywhere.

Memoization and retained caches

Memoization uses a closure to keep a cache:

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(...args);
    cache.set(key, result);
    return result;
  };
}

The returned function closes over cache and fn. This is powerful, but it can leak memory if the cache grows without bounds or keys retain large objects. In an interview, mention eviction, WeakMap for object keys, or limiting cache size when appropriate.

Closures are not memory leaks by themselves. They become leaks when long-lived functions retain references to data that should be collectible.

Stale closures in React and asynchronous code

Modern interviews often ask about stale closures. A stale closure happens when a callback closes over a value from an earlier render or earlier point in time, then runs later when the value has changed.

Example conceptually:

function SearchBox() {
  const [query, setQuery] = useState("");

  useEffect(() => {
    const id = setInterval(() => {
      console.log(query);
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

The interval callback closes over the initial query because the dependency array is empty. Fixes include adding dependencies, using functional state updates, storing mutable values in refs, or restructuring the effect.

For asynchronous code, the same idea applies. A callback may close over an old variable. Strong candidates explain the timing: “The function was created during an earlier render, so it sees that render's bindings.”

Closures and this are different concepts

Closures are about lexical variables. this is about call context, except for arrow functions, which capture this lexically. Interviewers sometimes mix them.

const obj = {
  value: 42,
  regular() { return this.value; },
  arrow: () => this.value
};

The arrow function does not bind its own this; it uses this from the surrounding scope. That is related to lexical capture, but it is not the same as closing over a local variable. Keep the distinction clear.

If asked why const self = this appears in old code, explain that it used closure to preserve a reference to this before arrow functions were common.

Common closure interview questions

What will this print? Trace bindings, not vibes. Identify where the function was created, which variables it references, and when it runs.

How do you create private variables? Return functions or methods that close over variables in an outer function.

How would you implement once? Use a closure to store whether the function has already run.

function once(fn) {
  let called = false;
  let value;
  return function (...args) {
    if (!called) {
      called = true;
      value = fn.apply(this, args);
    }
    return value;
  };
}

How would you implement debounce? Use a closure to store the timer ID.

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

These questions test whether you can use closures, not only define them.

Common traps and precise fixes

| Trap | Better answer | |---|---| | “Closures copy values” | They retain access to lexical bindings | | “Closures only happen when returning functions” | Any function referencing outer variables forms a closure | | Confusing scope with call site | JavaScript uses lexical scope | | Ignoring var function scope | Use let, IIFE, or pass arguments | | Treating all retained memory as leaks | It is a leak only if unwanted references stay reachable | | Mixing this and closure | Explain them separately |

When explaining a code sample, slow down. Mark each binding, when it is created, whether it is shared, and when the callback runs. That method beats memorizing outputs.

How to talk about closures on a resume or in interviews

Closures rarely belong as a standalone resume keyword unless the role is frontend-heavy. Better bullets connect closures to a real pattern:

  • “Implemented debounced search and request cancellation to reduce duplicate API calls during rapid typing.”
  • “Built a configurable validation library using function factories and closure-based rule composition.”
  • “Fixed stale-closure bugs in React effects by correcting dependencies and moving mutable request state into refs.”

In interviews, say: “I use closures for encapsulation, factories, callbacks, and async control, but I watch for stale captures and long-lived references.” That is practical and balanced.

Prep checklist

Before a JavaScript closures interview, practice the var loop question, an IIFE example, a private counter, once, debounce, memoization, and a stale React effect. Be ready to define lexical scope and scope chain. Be precise that closures capture bindings, not necessarily frozen values. If you can predict when a function is created and what environment it closes over, you can handle most closure questions confidently.