On this page · sections
JavaScript Execution Context & Closures
Can you explain what a function still sees after it returns — and why that captured binding is not a snapshot?
Execution context and closures are the same story: every call creates an environment, and inner functions keep a link to it.
These questions test:
- Call stack vs lexical environment
- Creation phase vs execution phase
- Capturing the binding, not the value
- Private state and lifetime
- Memory and stale closures in React
Questions
Context
Closures
Senior-Level Follow-ups
- Private State With Closures
- Does a Closure Keep the Whole Environment Alive?
- Stale Closures in React
Context
1. What Is an Execution Context?
Why This Matters
Hoisting, this, and closures all follow from how the engine builds an environment before running the first line.
Problem
What is created when a function is called, and how are identifiers resolved?
Example
function outer() {
const x = 10;
function inner() {
console.log(x);
}
inner();
}
outer();Creation phase
→ variable / lexical environment
→ this binding
→ hoist function declarations
Execution phase
→ run statements
→ resolve names by walking the outer chainIdentifier lookup walks:
current environment → outer → … → module / globalThat chain is lexical. It is determined by where the function was written, not by who called it.
Interview Answer
“An execution context is the environment for one run of a function or script. It holds bindings,
this, and an outer reference. The call stack tracks which context is running. Name resolution walks the lexical chain, not the caller chain.”
2. Call Stack & Recursion
Why This Matters
Synchronous recursion without a base case throws RangeError: Maximum call stack size exceeded. Deep JSON or tree walks hit the same limit.
Problem
What does the stack do on a recursive call, and should you rely on tail-call optimization?
Example
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial(5);Each call pushes a frame. Return pops it.
Tail-call reuse exists in the spec. Engines rarely apply it in practice. For unbounded depth, use an explicit stack or iteration.
Interview Answer
“The call stack is LIFO. Recursion costs a frame per call. I do not depend on TCO in browsers. For deep traversal I use an explicit stack so memory is data, not call-stack.”
Closures
3. What Is a Closure?
Why This Matters
The interview definition is not “a function inside a function.” It is a function plus the lexical environment it was created in.
Problem
Does a closure capture the value or the variable? Give a production example.
Example
function makeCounter() {
let count = 0;
return {
inc() {
return ++count;
},
read() {
return count;
},
};
}
const a = makeCounter();
const b = makeCounter();
a.inc();
a.inc();
b.inc();
console.log(a.read(), b.read());
// 2 1Each call to makeCounter creates a new environment.
Captures the binding:
let value = 1;
const read = () => value;
value = 2;
read(); // 2To freeze a value, copy it into a new binding:
const readFrozen = ((snapshot) => () => snapshot)(value);Key Idea
Closure = function object + [[Environment]]
1. It captures the BINDING, not a snapshot
2. It is created whenever an inner function uses an outer nameInterview Answer
“A closure is a function that retains its lexical environment. It sees later assignments to that binding. Two factory calls do not share state unless they close over the same environment.”
4. The var-in-a-Loop Bug
Why This Matters
This is still the most common closure output question.
Problem
Why do three timeouts print 3, and how do you fix it?
Example
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3 3 3Fixes:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0 1 2for (var i = 0; i < 3; i++) {
(function (copy) {
setTimeout(() => console.log(copy), 0);
})(i);
}Interview Answer
“
var iis one function-scoped binding. Every callback reads the final value.letgives each iteration its own binding. An IIFE withvarcopies the value into a new parameter.”
Senior-Level Follow-ups
5. Private State With Closures
Closures are how JavaScript had private fields before #:
function createWallet(opening) {
let balance = opening;
return {
deposit(n) {
balance += n;
},
getBalance() {
return balance;
},
};
}balance is unreachable except through the returned methods. That is encapsulation, not a security boundary against hostile code in the same realm.
Class #fields are the language-level version. Closures still win for factories and one-off APIs.
6. Does a Closure Keep the Whole Environment Alive?
In theory the environment record stays reachable. Engines may optimize unused bindings, but you must not rely on that.
function load() {
const huge = new Array(1e6).fill("x");
const id = 1;
return () => id;
}If huge is not used by the returned function, some engines can drop it. If anything in the closure graph still mentions it, it stays.
For caches and event handlers, assume closed-over objects live as long as the function.
7. Stale Closures in React
function Counter() {
const [n, setN] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(n);
}, 1000);
return () => clearInterval(id);
}, []);
}The interval closes over the first n. It will log 0 forever.
Fixes: include n in deps, or read from a ref, or use the functional updater.
This is the same rule: the function captures the binding from the render that created it.
Final Interview Strategy
1. Context = environment for one run
2. Stack tracks contexts; lexical chain resolves names
3. Closure = function + environment
4. Capture is by binding, not snapshot
5. var loop vs let loop is one vs many bindings
6. Closed-over data has lifetime cost
7. React stale props/state are the same closure ruleSenior-level signal: Define closure precisely, then connect it to memory, privacy, and UI staleness — not only to the loop puzzle.