JavaScript Closures and Scope Interview Questions: 10 Questions with Clear Answers
Understanding scope and closures is a primary evaluation benchmark in front-end technical interviews. These concepts appear consistently across technical phone screens, core JavaScript screenings, and React architecture rounds.
Interviewers evaluate whether a candidate understands the JavaScript execution model, memory retention, and asynchronous closures rather than surface syntax.
This guide targets front-end developers with 0 to 5 years of experience preparing for junior to mid-level engineering interviews.
| # | Question | Level |
|---|---|---|
| 1 | What is a closure in JavaScript, and how does lexical scoping make it work? | Junior |
| 2 | What is the difference between global scope, function scope, and block scope? | Junior |
| 3 | What is variable hoisting, and how does it differ between var, let, and const? | Junior |
| 4 | What is the Temporal Dead Zone (TDZ), and why does it exist? | Junior |
| 5 | How does a for loop containing setTimeout behave with var versus let, and why? | Mid |
| 6 | How can closures be used to create private state and emulate encapsulation? | Mid |
| 7 | What is function currying, and how do closures enable partial application? | Mid |
| 8 | What causes a stale closure in React hooks, and how do you resolve it? | Mid |
| 9 | How do closures interact with garbage collection, and how can they introduce memory leaks? | Senior |
| 10 | How does the ECMAScript specification define Lexical Environments and the outer environment reference chain? | Senior |
Q1. What is a closure in JavaScript, and how does lexical scoping make it work?
Level: Junior
What the interviewer is checking: The interviewer wants to verify that you know the exact definition of a closure and understand that functions retain access to their defining lexical environment rather than their execution call site.
Short answer (say this first): A closure is the combination of a function bundled together with references to its surrounding lexical state. In JavaScript, an inner function retains access to variables declared in its outer scope even after that outer function has returned. This works because JavaScript uses lexical scoping, where variable resolution is determined by where functions are written in source code.
Detailed explanation:
- Lexical Scoping: Variable visibility is locked at parse time based on source code structure. Inner blocks resolve variables in outer blocks, but outer scopes cannot look inward.
- Environment Retention: When an inner function outlives its parent and references outer variables, the engine preserves that lexical environment record on the heap.
- Live Bindings: Closures hold live references to outer variables, not static value snapshots. Mutating an outer variable is immediately reflected in the closure.
Example:
function createCounter(initial = 0) {
let count = initial;
return () => ++count;
}
const counter = createCounter(5);
console.log(counter()); // Output: 6
console.log(counter()); // Output: 7
Common mistakes:
- Saying closures snapshot or copy variable values at creation time instead of holding live references to the environment record.
Likely follow-up questions:
- Does every JavaScript function form a closure? — Yes, specification-wise, every function holds an internal
[[Environment]]slot referencing its defining lexical environment.
Q2. What is the difference between global scope, function scope, and block scope?
Level: Junior
What the interviewer is checking: The interviewer is confirming your understanding of variable visibility boundaries across legacy and modern ECMAScript standards.
Short answer (say this first):
Global scope variables are declared outside all functions and blocks, making them accessible everywhere. Function scope confines variables declared with var strictly to the enclosing function body, ignoring nested blocks. Block scope, introduced in ES6 for let and const, confines variables strictly within the nearest enclosing curly braces ({}).
Detailed explanation:
- Global Scope: Accessible across scripts. In browsers, top-level
varattaches towindow, whileletandconstdo not. In ES modules, top-level variables remain module-scoped. - Function Scope: Variables declared with
varare accessible anywhere inside the function, escapingifandforblocks. - Block Scope: Any block enclosed by
{}isolateslet,const, and classes from the surrounding scope.
Example:
function scopeDemo() {
if (true) {
var funcVar = 'escapes block';
let blockVar = 'confined to block';
}
console.log(funcVar); // Output: 'escapes block'
// console.log(blockVar); // ReferenceError: blockVar is not defined
}
scopeDemo();
Common mistakes:
- Assuming that wrapping code in an
ifblock preventsvarvariables from leaking into the outer function scope.
Likely follow-up questions:
- Are
catchparameters block-scoped? — Yes, the error parameter intry...catch(err)is scoped strictly to thecatchblock.
Q3. What is variable hoisting, and how does it differ between var, let, and const?
Level: Junior
What the interviewer is checking: The interviewer wants to assess whether you understand the compilation and execution phases of the JavaScript engine.
Short answer (say this first):
Hoisting is the engine's behavior of reserving memory for declarations during compilation before execution starts. Declarations with var are hoisted and initialized immediately to undefined. In contrast, let and const are hoisted into an uninitialized state, meaning accessing them before their declaration line throws a ReferenceError.
Detailed explanation:
- Function Declarations: The entire function body is hoisted and initialized, permitting safe calls before the declaration line.
varDeclarations: Registered and initialized toundefinedduring compilation; assignments execute at runtime.letandconstDeclarations: Registered in scope during compilation but left uninitialized until evaluated, creating the Temporal Dead Zone.
Example:
console.log(hoistedVar); // Output: undefined
var hoistedVar = 'initialized';
try {
console.log(hoistedLet);
} catch (err) {
console.log(err.name); // Output: ReferenceError
}
let hoistedLet = 'initialized';
Common mistakes:
- Claiming
letandconstare not hoisted. They are hoisted, which is why accessing them throws aReferenceErrorinstead of falling back to an outer global variable.
Likely follow-up questions:
- What happens if a function declaration and
varshare the same name? — The function declaration takes precedence during hoisting; a subsequentvardeclaration without assignment does not overwrite it.
Q4. What is the Temporal Dead Zone (TDZ), and why does it exist?
Level: Junior
What the interviewer is checking: The interviewer wants to verify that you understand modern ECMAScript safety guarantees and why accessing uninitialized bindings is forbidden.
Short answer (say this first):
The Temporal Dead Zone is the period between entering a scope and evaluating the initialization of a let or const variable. Any read or write access during this window throws a ReferenceError. It was introduced to catch reference bugs early and to preserve the invariant that const variables are never observed without their assigned value.
Detailed explanation:
- Temporal Nature: The TDZ is based on execution order in time, not physical code lines. Calling a function above the declaration succeeds if the invocation happens after initialization.
- Design Rationale: Returning
undefinedfor uninitializedvarvariables masked bugs. Forconst, initializing toundefinedbefore assignment would violate immutability guarantees. typeofBehavior: Usingtypeofon an undeclared variable returns'undefined', but usingtypeofon a variable trapped in the TDZ throws aReferenceError.
Example:
function tdzCheck() {
const readVar = () => val;
// console.log(val); // Throws ReferenceError (in TDZ)
let val = 'Ready';
console.log(readVar()); // Output: 'Ready' (invoked after TDZ ends)
}
tdzCheck();
Common mistakes:
- Believing that
typeofis always safe and never throws an error in modern JavaScript.
Likely follow-up questions:
- Do default parameters have a TDZ? — Yes, parameters evaluate left-to-right; setting
function fn(a = b, b = 1) {}triggers a TDZReferenceErrorforb.
Q5. How does a for loop containing setTimeout behave with var versus let, and why?
Level: Mid
What the interviewer is checking: The interviewer is testing your combined knowledge of the event loop, lexical scoping, and per-iteration loop bindings.
Short answer (say this first):
With var, a single function-scoped variable is shared across all iterations. When asynchronous setTimeout callbacks run after loop completion, they all reference that single variable after it has reached its terminal value (e.g., printing 3, 3, 3). With let, the engine creates a fresh lexical binding for every iteration, allowing each callback closure to capture its distinct iteration value (printing 0, 1, 2).
Detailed explanation:
varExecution: The loop variableiis hoisted to the enclosing function. Synchronous iterations incrementito3. When macrotask timer callbacks execute, they all look up the final shared binding3.letExecution: The ECMAScript specification dictates thatfor (let ...)creates a fresh lexical environment record per iteration, copying the current value forward. Each closure retains its own independent binding.
Example:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 10);
}
// Output: var: 3, var: 3, var: 3
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j), 10);
}
// Output: let: 0, let: 1, let: 2
Common mistakes:
- Assuming that
setTimeoutwith a delay of0executes before the next synchronous iteration of the loop.
Likely follow-up questions:
- How did developers solve this before ES6? — By passing
iinto an IIFE to capture the current value in a function scope closure, or by passingias the third parameter tosetTimeout.
Q6. How can closures be used to create private state and emulate encapsulation?
Level: Mid
What the interviewer is checking: The interviewer wants to see how you leverage closures for data privacy, protect invariants, and contrast closures with native # private class fields.
Short answer (say this first): Closures create private state by declaring variables inside an outer factory function and returning an object containing methods that access those variables. Because the variables are not object properties, external code cannot access or mutate them directly. They can only be inspected or modified through the exposed privileged methods.
Detailed explanation:
- Factory Encapsulation: Variables declared with
letinside a factory are unreachable from outside. Inner methods close over these variables, forming secure private state. - Security: Unlike properties, closed-over variables cannot be inspected via
Object.keys()or reflection. - Trade-off: Each instance created via a factory allocates separate method functions in memory. Native
#private class fields allow methods to live on the prototype while maintaining engine-enforced privacy.
Example:
function createAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) throw new Error('Must be positive');
balance += amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const acct = createAccount(100);
console.log(acct.deposit(50)); // Output: 150
console.log(acct.balance); // Output: undefined
Common mistakes:
- Returning a mutable reference (such as an internal array or object) directly from a getter method without cloning, allowing external mutation by reference.
Likely follow-up questions:
- How do
#private fields differ under the hood? — Private fields are stored in internal slots tied to the instance description, allowing methods to remain shared on the class prototype.
Q7. What is function currying, and how do closures enable partial application?
Level: Mid
What the interviewer is checking: The interviewer is evaluating your functional programming skills and your ability to construct higher-order functions that store state across invocations.
Short answer (say this first): Currying transforms a function accepting multiple arguments into a sequence of unary functions that each accept a single argument. Partial application fixes a subset of arguments upfront, returning a function that accepts the remainder. Closures enable both techniques by storing supplied arguments in enclosing lexical environments until all required parameters have been received.
Detailed explanation:
- Closure Mechanism: Each nested call captures incoming arguments in its lexical scope. Intermediate functions return further functions, preserving previous arguments until
args.length >= fn.length. - Distinction: Currying strictly produces unary functions ($f(a)(b)(c)$), whereas partial application binds one or more parameters and accepts all remaining arguments in one call ($f(a, b)(c)$).
- Front-End Utility: Useful for configuring reusable event handlers, API query wrappers, and data transformations.
Example:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...next) => curried.apply(this, args.concat(next));
};
}
const multiply3 = (a, b, c) => a * b * c;
const curriedMult = curry(multiply3);
console.log(curriedMult(2)(3)(4)); // Output: 24
console.log(curriedMult(2, 3)(4)); // Output: 24
Common mistakes:
- Confusing currying with partial application, or writing currying helpers that break when the target function uses rest parameters (
...args) wherefn.lengthis0.
Likely follow-up questions:
- How does
fn.lengthbehave with default parameters? —fn.lengthonly counts parameters before the first default parameter, ignoring rest parameters.
Q8. What causes a stale closure in React hooks, and how do you resolve it?
Level: Mid
What the interviewer is checking: This is a vital question for React developers. The interviewer wants to verify that you understand how React component render lifecycles interact with JavaScript closures.
Short answer (say this first):
A stale closure in React occurs when an asynchronous callback, subscription, or hook effect closes over state or props from an earlier render without refreshing when those values change. The callback continues reading the outdated value from the older render's lexical environment. You resolve it by specifying complete dependency arrays, using functional state updaters (setState(prev => ...)), or storing mutable values in useRef.
Detailed explanation:
- Render Lifecycles: Functional components create a fresh lexical scope on every render with new state and prop variables.
- Cause of Staleness: If an effect with
[]dependencies sets an interval that referencescount, that interval callback permanently closes overcountfrom the initial mount render. - Resolution Techniques:
- Functional State Updates: Pass
setCount(prev => prev + 1)so the update reads React's internal queue directly. - Exhaustive Dependencies: Include all referenced reactive values in the dependency array.
- Refs: Use
useRefto store values on.current, which is stable across renders.
- Functional State Updates: Pass
Example:
import React, { useState, useEffect } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// Functional updater prevents stale closure without needing count in deps
const timer = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>Count: {count}</div>;
}
Common mistakes:
- Disabling the
react-hooks/exhaustive-depslinter rule instead of using functional updates or ref patterns.
Likely follow-up questions:
- How does modern React handle non-reactive logic inside effects? — Modern React recommends extracting non-reactive logic into custom ref wrappers or
useEffectEventto read fresh state without re-triggering effects.
Q9. How do closures interact with garbage collection, and how can they introduce memory leaks?
Level: Senior
What the interviewer is checking: The interviewer wants to assess whether you understand mark-and-sweep garbage collection, reachability roots, and how closures can inadvertently pin large memory allocations.
Short answer (say this first): JavaScript garbage collectors free memory by tracking object reachability from root references using mark-and-sweep algorithms. A closure retains its enclosing Lexical Environment Record on the heap as long as the inner function is reachable. If a long-lived object—such as a global event listener, timer, or SPA singleton—holds a reference to a closed-over function, all variables in that lexical environment remain reachable and cannot be garbage collected.
Detailed explanation:
- Retained Lexical Scope: When a function outlives its parent, the engine preserves the parent environment record on the heap.
- Shared Environments in V8: Closures created within the same scope often share an environment record. An active small listener can inadvertently keep a large unused variable in the same scope alive in memory.
- Mitigation: Remove event listeners on component unmount, clear active intervals, set unused heavy references to
null, and leverageWeakMaporWeakSetwhere appropriate.
Example:
function setupListener() {
const largeData = new Array(1000000).fill('payload');
const handler = () => console.log('Length:', largeData.length);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}
const teardown = setupListener();
teardown(); // Memory now eligible for garbage collection
Common mistakes:
- Assuming that setting a local variable to
nullinside an uncalled inner function immediately frees memory.
Likely follow-up questions:
- How do you diagnose closure leaks in Chrome DevTools? — Take two heap snapshots in the Memory panel before and after an action, filter by "Objects allocated between Snapshot 1 and 2", and inspect the Retainer tree.
Q10. How does the ECMAScript specification define Lexical Environments and the outer environment reference chain?
Level: Senior
What the interviewer is checking: The interviewer is probing your mastery of language specification internals, evaluating whether you understand how the engine models the scope chain under the hood.
Short answer (say this first):
The ECMAScript specification defines a Lexical Environment as a specification mechanism consisting of two parts: an Environment Record, which stores identifier bindings, and an outer reference ([[OuterEnv]]), which points to the enclosing Lexical Environment. When resolving an identifier, the engine inspects the current Environment Record; if unresolved, it traverses the [[OuterEnv]] chain recursively until reaching the Global Environment, throwing a ReferenceError if still unfound.
Detailed explanation:
- Environment Records: Concrete storage mechanisms for bindings. These include Declarative Environment Records (for
let,const, function declarations) and Object Environment Records (for the global object withvar). - Function
[[Environment]]Slot: When a function object is instantiated, the engine stores the running execution context's Lexical Environment in the function's internal[[Environment]]slot. - Invocation Scope Chain: Calling the function creates a new execution context whose
[[OuterEnv]]is wired directly to that stored[[Environment]]slot, creating a linked list of scopes (BlockEnv -> FunctionEnv -> GlobalEnv -> null).
Example:
const globalName = 'global';
function outer() {
const outerName = 'outer';
function inner() {
const innerName = 'inner';
return `${innerName} -> ${outerName} -> ${globalName}`;
}
return inner();
}
console.log(outer()); // Output: 'inner -> outer -> global'
Common mistakes:
- Conflating the execution call stack with the scope chain. The call stack tracks dynamic execution order, while the scope chain is determined at author time by lexical nesting.
Likely follow-up questions:
- What is the difference between
LexicalEnvironmentandVariableEnvironmentin an Execution Context? — Both are Lexical Environments;VariableEnvironmentholds legacyvardeclarations, whileLexicalEnvironmentholds modern block-scoped declarations (let,const) and updates when entering nested blocks.
Preparation Checklist
- Practise reciting the 2-sentence definition of a closure out loud without checking notes.
- Diagram the classic
forloop withsetTimeoutusingvarandletstep-by-step through the event loop. - Build a generic
curry()utility function from scratch in TypeScript or JavaScript. - Audit a React component for stale closures in
useEffectand fix it using a functional state updater. - Inspect a closure retention tree in Chrome DevTools using heap snapshots.
- Explain the difference between the runtime execution call stack and the compile-time scope chain out loud.
Conclusion
Closures and lexical scoping are the foundational mechanisms governing state retention, encapsulation, and asynchronous timing in JavaScript. By mastering both the conceptual mechanics—how lexical environments persist on the heap—and practical patterns like React stale closures and memory management, you will be prepared to answer interview questions with precision and technical confidence.