Senior 6 min · March 06, 2026

JavaScript Closures — Silent Memory Leak from DOM Nodes

RSS memory grew for 6h until OOMKill — closures captured jsdom DOM nodes.

N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Written from production experience, not tutorials.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Closures bundle a function with its lexical environment — the variables in scope when the function was defined, not invoked
  • The [[Environment]] internal slot on every function points to the scope chain at definition time
  • The loop bug: var creates one binding per loop, so all callbacks share the same i — use let for per-iteration bindings
  • Memory cost: closures keep the entire context alive — one large variable in a shared scope prevents GC of everything else
  • Production insight: stale closures in React hooks cause silent state bugs — fix with functional updates or useRef
✦ Definition~90s read
What is JavaScript Closures Interview Q?

A closure is a function that retains access to variables from its outer (enclosing) scope even after that outer function has finished executing. JavaScript engines create closures by storing a reference to the function's entire lexical environment — including any DOM nodes, objects, or primitives in scope — rather than copying values.

Imagine you work at a coffee shop and you're given a notepad with the day's special price written on it.

This is why closures are the root cause of silent memory leaks in single-page applications: when an event handler closure captures a DOM node reference, that node can never be garbage-collected even after it's removed from the DOM tree, because the closure's environment still holds a live reference to it. In production React apps, stale closures in useEffect or useCallback cause the same pattern — captured state values become frozen at the time the closure was created, leading to both memory leaks and stale UI bugs.

The fix is always the same: ensure closures capture only what they need, and explicitly nullify references to DOM nodes or large objects when the closure's lifecycle ends.

Plain-English First

Imagine you work at a coffee shop and you're given a notepad with the day's special price written on it. Even after you leave the shop and go home, you still have that notepad — you can read the price anytime. A JavaScript closure is exactly that: a function that 'takes its notepad home' — it remembers the variables from where it was created, even after that original context is long gone. The function and its remembered environment are inseparable.

Closures are the single most interrogated concept in JavaScript interviews — and for good reason. They're not just a language quirk; they're the engine behind module patterns, memoization, event handlers, React hooks, and virtually every callback-heavy system you'll write in production. If you don't own closures cold, you'll struggle to reason about async bugs, memory leaks, and stateful logic at scale.

The problem closures solve is deceptively simple: how does a function retain access to variables that were defined in a scope that has already finished executing? In most mental models of code, once a function returns, its local variables evaporate. Closures break that assumption in the most useful way possible — they keep a live reference to the surrounding lexical environment, not a snapshot, not a copy.

By the end of this article you'll be able to explain the V8-level mechanics of how closures are stored, identify the three classic closure interview traps (the loop bug, the memory leak, and the stale reference), write module patterns and memoization from scratch, and answer the follow-up questions that trip up even experienced devs. Let's build this from the engine up.

What JavaScript Closures Actually Capture — and Why DOM Nodes Leak

A closure is a function bundled with its lexical environment — the variables in scope at definition time. The core mechanic: inner functions retain references to outer variables even after the outer function returns. This isn't magic; it's how the engine preserves the scope chain for execution. Every closure holds a live reference to the entire outer scope, not a snapshot. That means if an outer variable is a DOM node, the closure keeps that node alive in memory. The garbage collector cannot reclaim it until every closure referencing that scope is itself unreachable. In practice, this turns event handlers, callbacks, and timers into accidental memory anchors. A single closure attached to a detached DOM subtree prevents the entire subtree from being freed. The cost is not theoretical — it's measurable heap growth in long-lived pages. Use closures intentionally: capture only what you need, nullify references when the component unmounts, and avoid capturing large objects or DOM nodes in long-lived closures.

Closure ≠ Snapshot
A closure captures a live reference to the variable, not its value at closure creation time. That's why DOM nodes stay alive — the reference is active.
Production Insight
A React component with a setInterval callback that captures this.state — the closure keeps the old component instance alive, causing a memory leak that grows linearly with user interactions.
Symptom: Chrome DevTools heap snapshot shows detached DOM trees with 'Detached InternalNode' entries that never decrease after component unmount.
Rule of thumb: If a closure outlives the component lifecycle, explicitly nullify any DOM or large object references inside it.
Key Takeaway
Closures retain the entire outer scope, not just the variables you use.
A single closure referencing a DOM node prevents the entire subtree from being garbage collected.
Always clean up closures in useEffect return or addEventListener remove — or accept the leak.
Closure Memory Leak from DOM Nodes THECODEFORGE.IO Closure Memory Leak from DOM Nodes How closures capture DOM references and cause leaks Closure Captures Variable Not value, but reference to variable DOM Node Reference Held Closure retains reference to DOM element Event Handler Closure Handler closure captures DOM node Memory Leak Occurs DOM node cannot be garbage collected Nullify References Set captured variables to null after use ⚠ Event handlers in closures keep DOM alive Always remove listeners or nullify references to avoid leaks THECODEFORGE.IO
thecodeforge.io
Closure Memory Leak from DOM Nodes
Javascript Closures Interview Questions

How the JavaScript Engine Actually Creates a Closure

When the JS engine parses a function, it records the function's lexical environment — the scope chain that was active at the point of definition, not the point of invocation. This is baked into the function object itself as an internal [[Environment]] slot. You can't read it directly, but it's always there.

When the outer function returns, its Execution Context is popped off the call stack. Normally the local variables would be garbage-collected. But if any inner function holds a reference to those variables through its [[Environment]] slot, the garbage collector sees them as still reachable — so it keeps them alive in a structure called a closure record (or context object in V8 terms).

This is why closures aren't 'magic' — they're a predictable consequence of lexical scoping plus garbage collection. The engine asks one question: 'Is anyone still pointing at this variable?' If yes, it stays alive.

Critically, the closure captures variables by reference, not by value. If the outer variable mutates after the closure is created, the closure sees the new value. This is the root cause of the infamous loop-closure bug and every stale-value head-scratcher you'll encounter in production.

io/thecodeforge/js/closure_internals.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * @package io.thecodeforge
 * Demonstration of shared variable reference in closures
 */

function createCounterPair() {
  let count = 0; // Lives in the heap-allocated closure record

  function increment() {
    count += 1; // Mutating the shared reference
    return count;
  }

  function decrement() {
    count -= 1;
    return count;
  }

  return { increment, decrement };
}

const counter = createCounterPair();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1 (sees the mutated state)

const independent = createCounterPair();
console.log(independent.increment()); // 1 (isolated scope)
Output
1
2
1
1
Interview Gold:
When asked 'what is a closure?', most candidates say 'a function inside a function'. The correct answer is: 'A closure is a function bundled together with its lexical environment — specifically the set of variable bindings that were in scope when the function was defined.' Mention the [[Environment]] internal slot and watch the interviewer's eyebrows rise.
Production Insight
In V8, closures are heap-allocated context objects, not stack frames.
The GC keeps the entire context alive if any inner function references any part of it.
Rule: to minimize memory, avoid capturing large arrays in closures used in hot paths.
Key Takeaway
Closures capture variable bindings by reference, not value.
Every function has an internal [[Environment]] slot that points to its lexical scope chain.
The GC determines closure lifetime — closures keep variables alive as long as they're reachable.

The Classic Loop-Closure Bug — and Three Ways to Fix It

This is the most-asked closure question in JavaScript interviews, bar none. It seems simple, and that's exactly what makes it dangerous. The bug happens because var is function-scoped. Every iteration of the loop doesn't create a new variable — it mutates the same memory location. By the time the callbacks fire, the loop has completed and the variable holds its final value.

There are three idiomatic fixes: using let (block-scoped), using an IIFE to create a new scope per iteration, or using .forEach. Understanding why let works is key: the ES6 spec mandates that a let variable in a for loop header is re-bound for every iteration of the loop.

io/thecodeforge/js/loop_solutions.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * @package io.thecodeforge
 * Senior-level loop closure solutions
 */

// Solution 1: Block Scoping (Modern Standard)
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log('let:', i), 100);
}

// Solution 2: IIFE (Immediate Invoked Function Expression)
for (var i = 0; i < 3; i++) {
    (function(capturedIndex) {
        setTimeout(() => console.log('iife:', capturedIndex), 100);
    })(i);
}

// Solution 3: Argument Binding via .bind()
for (var i = 0; i < 3; i++) {
    setTimeout(console.log.bind(console, 'bind:', i), 100);
}
Output
let: 0
let: 1
let: 2
iife: 0
iife: 1
iife: 2
bind: 0
bind: 1
bind: 2
Watch Out:
The let fix works because the ECMAScript spec mandates that let in a for loop header creates a new binding for each iteration — it's not just block-scoping the variable, it's actively re-binding it. If you use let in a while loop without manually reassigning, you don't get this guarantee.
Production Insight
Production bugs from the loop closure often appear in async flows: array of Promises, event listeners in loops, or batch API calls.
The fix with let is zero-overhead — the spec creates new bindings per iteration with negligible memory cost.
Rule: always use let in for loops that create callbacks, not var.
Key Takeaway
var is function-scoped — one binding per function, not per loop iteration.
let in a for loop creates a new binding each iteration — this fixes the closure bug.
The three fixes: let, IIFE, or bind — choose let for clarity.

Closures in Production — Module Pattern, Memoization & Memory Pitfalls

Closures allow for powerful patterns like the Module Pattern (private state) and Memoization (caching). However, they come with a memory cost. Because a closure keeps its entire [[Environment]] alive, a large object captured in a closure will never be garbage-collected as long as the closure exists.

In V8, if multiple inner functions are defined in the same scope, they share a single context object. This means if one inner function captures a large array, that array stays in memory even for other functions that don't use it, provided they were created in the same environment.

io/thecodeforge/js/memoization.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * @package io.thecodeforge
 * Production-grade Memoization via Closures
 */

const memoize = (fn) => {
    const cache = new Map();
    return (...args) => {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
};

const expensiveOperation = memoize((num) => {
    console.log("Computing...");
    return num * 2;
});

console.log(expensiveOperation(10)); // Computing...
console.log(expensiveOperation(10)); // (Cached)
Output
Computing...
20
20
Pro Tip:
In V8, if a closure doesn't actually reference a variable from the outer scope, the engine is smart enough not to retain it — this is called 'closure elision'. But the moment any inner function in a shared scope references a variable, V8 must keep the entire context alive.
Production Insight
A common production pitfall: creating closures inside a hot loop that capture large arrays. Each closure keeps its own context, leading to memory bloat.
Profile with heap snapshots: look for (context) entries that contain arrays bigger than expected.
Rule: in performance-critical paths, pass data as arguments instead of capturing it in a closure.
Key Takeaway
Modules and memoization rely on closures for private state.
One large captured variable keeps the entire shared context alive.
Closure elision helps, but only if no inner function references the variable.

Stale Closures in React Hooks — The Modern Production Gotcha

Stale closures occur when a function 'remembers' a variable from an old render cycle. In React, because useEffect and useCallback create closures, if the dependency array is incorrect, the closure holds onto old state values.

To fix stale closures, developers use functional updates in setState or the useRef pattern, which provides a stable reference that always points to the latest value without requiring the closure to be re-created.

io/thecodeforge/js/react_fix.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * @package io.thecodeforge
 * Fixing stale closures in React hooks
 */

// Functional Update Fix
const [count, setCount] = useState(0);

useEffect(() => {
    const id = setInterval(() => {
        // Bypasses the closure by using the state updater callback
        setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(id);
}, []); // Safe now
Output
// Count increments correctly regardless of initial closure
Watch Out:
The React team's ESLint plugin eslint-plugin-react-hooks enforces exhaustive dependency arrays precisely to catch stale closures at lint time. Disabling that rule with // eslint-disable-next-line is almost always the wrong call.
Production Insight
React's exhaustive-deps lint rule catches 95% of stale closures at compile time.
When you see 'React Hook useEffect has a missing dependency', don't suppress it — fix the dependency array or use functional updates.
Rule: the safest pattern for intervals/timeouts is setCount(prev => prev + 1) — no closure dependency needed.
Key Takeaway
Stale closures in hooks capture values from an old render cycle.
Functional updates in setState circumvent the closure by using a callback.
useRef provides a stable container that always points to the current value.

Closures in Event Handlers — The Hidden Memory Leak Pattern

Event listeners attached to DOM nodes (or Node.js EventEmitter objects) create closures that capture the surrounding scope. If the listener is never removed, the entire captured scope stays in memory — including the DOM node itself, preventing it from being garbage-collected when it's removed from the document.

This is the single most common closure-related memory leak in browser apps. The fix is simple: always pair every addEventListener with a corresponding removeEventListener in the cleanup phase. For React, use the cleanup function returned from useEffect. For plain JS, use addEventListener's { once: true } option for one-shot events.

io/thecodeforge/js/event_listener_cleanup.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * @package io.thecodeforge
 * Safe event listener pattern with closure cleanup
 */

// Bad: listener never removed, leaks scope
function addBadHandler(button, data) {
  button.addEventListener('click', function handleClick() {
    console.log(data); // captures `data` forever
  });
  // button removed later but handleClick still references data
}

// Good: remove listener explicitly
function addGoodHandler(button, data) {
  function handleClick() {
    console.log(data);
  }
  button.addEventListener('click', handleClick);
  
  // Later, when button is removed:
  // button.removeEventListener('click', handleClick);
  // Or in a React useEffect cleanup:
  // return () => button.removeEventListener('click', handleClick);
}

// Best for one-shot: use { once: true }
button.addEventListener('click', () => console.log(data), { once: true });
Output
// No output example — pattern demonstration
Interview Question:
Senior interviewers love asking: 'How do closures cause memory leaks in the browser?' The answer: an unremoved event listener that captures a large object in its closure keeps that object (and the whole scope chain) alive, even if the DOM node is removed. The fix is always to call removeEventListener in the cleanup.
Production Insight
In a Single Page App, navigating between routes often replaces DOM nodes. If event listeners are not cleaned up, each navigation leaks memory.
Chrome DevTools heap snapshot filter by 'Detached DOM Tree' will show these leaked nodes.
Rule: always pair addEventListener with removeEventListener in your component lifecycle.
Key Takeaway
Every addEventListener must have a matching removeEventListener.
Use { once: true } for events that fire once.
Detached DOM trees in heap snapshots often point to uncleaned closure-capturing listeners.

Closures Capture Variables, Not Values — Here's Why That Bites You

Most devs think closures capture a snapshot of a variable's value. That's wrong. Closures capture the variable itself — the reference. This is why the classic loop bug happens, and why seemingly innocent code can produce baffling output. When a closure runs, it reads the current value of the captured variable, not the value it had when the closure was created. If the variable is reassigned between creation and invocation, the closure sees the new value. This is the root cause of stale closure bugs in event handlers, timers, and async callbacks. Understanding that closures capture variable bindings (not values) is the single most important mental model shift you can make. It explains why let fixes the loop bug — it creates a new binding for each iteration — while var doesn't.

closure-variable-reference.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// Closures capture the variable, not the value
function createClosures() {
  const funcs = [];
  let x = 'apple';
  funcs.push(() => console.log(x));
  x = 'banana';
  funcs.push(() => console.log(x));
  return funcs;
}
const [fn1, fn2] = createClosures();
fn1(); // 'banana'
fn2(); // 'banana'
Output
banana
banana
Production Trap:
This bites you hard with async patterns. If you push closures into an array inside a loop and call them after the loop finishes, every closure will see the final value of the loop variable. Always use let or an IIFE to capture the current iteration's value.
Key Takeaway
Closures grab the variable binding, not a freeze-frame of its value.

The Closure Chain — How Nested Closures Create a Scope Ladder

A common interview curveball is nested closures. Each inner closure doesn't just capture the immediate outer function's variables — it captures the entire scope chain. This creates a ladder of accessible variables from the innermost function all the way out to the global scope. The key insight: closures don't copy variables up the chain; they maintain a reference to the entire lexical environment. This means modifying a variable in an outer scope from a deeply nested closure affects all closures at that level. It also means memory can leak if any closure in the chain outlives its parent scope. Interviewers love asking about this because it tests whether you understand scope chaining, not just single-level closure mechanics.

nested-closure-chain.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
function outer(x) {
  return function middle(y) {
    return function inner(z) {
      return `x=${x}, y=${y}, z=${z}`;
    };
  };
}
const withX = outer(10);
const withXY = withX(20);
console.log(withXY(30)); // 'x=10, y=20, z=30'
Output
x=10, y=20, z=30
Memory Insight:
Each nested closure retains its own Lexical Environment object. In this example, inner keeps a reference to both middle's environment (with y) and outer's environment (with x). If inner lives forever (e.g., in a callback), all three scopes stay in memory.
Key Takeaway
Nested closures are scope chains, not isolated bubbles — each one holds the entire ladder.

Closures in Event Handlers — The Hidden Memory Leak Pattern

The most common production leak with closures happens in event handlers. Attach a closure-based handler to a DOM element, and if the handler captures a reference to the element itself (via this or a variable), you've created a cyclic reference. The element holds a reference to the handler (via the event system), and the closure holds a reference back to the element (via the captured scope). Modern garbage collectors can handle simple cycles, but if that closure also captures a large data structure — like a massive state object or a DOM subtree — the element and all its captured data stay alive until the handler is explicitly removed. This is why single-page apps leak memory like sieves. The fix: always use addEventListener with a named function you can removeEventListener on, or use AbortController in modern browsers.

event-handler-leak.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function addLeakyHandler() {
  const hugeData = new Array(1000000).fill('leak');
  const button = document.getElementById('myButton');
  button.addEventListener('click', function handler() {
    console.log(hugeData.length);
  });
  // button is removed from DOM, but handler still
  // references it via the event system, and hugeData
  // stays alive because handler's closure captured it.
}

// Fix: Use AbortController for cleanup
function addCleanHandler() {
  const controller = new AbortController();
  const button = document.getElementById('myButton');
  button.addEventListener('click', () => {
    console.log('clicked');
  }, { signal: controller.signal });
  // Later: controller.abort() removes ALL handlers
}
Output
1000000
Production Trap:
Even removing a DOM element from the page doesn't remove its event listeners. The listener closure, with all its captured data, lives on until the element is garbage collected. If the element is never reaped (e.g., referenced by another closure), you have a permanent leak.
Key Takeaway
Closures in event handlers are the #1 cause of memory leaks in SPAs — always detach or use signals.
● Production incidentPOST-MORTEMseverity: high

A Silent Memory Leak: Closures Holding DOM Nodes

Symptom
RSS memory grew linearly over time — no plateau. After 6 hours, memory exceeded container limit and Kubernetes OOMKilled the pod. Heap snapshots showed thousands of detached DOM nodes (in a server-side app? Actually, the team used jsdom for server-side rendering and the closures kept the window objects alive).
Assumption
The team assumed that once the request handler returned, all local variables would be GC'd. They didn't account for the event listener attached to a jsdom window object that still referenced the large dataset via closure.
Root cause
The event listener was added with window.addEventListener('resize', handler), where handler was a closure that captured the entire data variable from the outer scope. The listener was never removed, so the window object and all its DOM tree stayed reachable even after the response was sent. The closure's [[Environment]] kept data alive, and data kept references to the DOM.
Fix
Remove the event listener after the response is sent using window.removeEventListener. Alternatively, use { once: true } for one-shot events, or structure the code so the closure doesn't capture large variables directly (pass only necessary data). In the incident, they switched to using a WeakMap to associate data with the window object and cleaned up in a finally block.
Key lesson
  • Every DOM event listener that captures data in a closure must be explicitly removed when no longer needed.
  • Use once: true for single-use event handlers to avoid the removal overhead.
  • Prefer passing minimal data into callbacks rather than capturing large objects from the outer scope.
  • Memory profiles are your friend — heap snapshots showing many detached DOM trees point directly at closure retention.
Production debug guideSymptom → Action grid for the three most common closure failures3 entries
Symptom · 01
All setTimeout/setInterval callbacks log the same final value
Fix
You have the classic loop bug. Change var to let in the loop header, or wrap the callback in an IIFE that captures the current iteration value.
Symptom · 02
React component shows stale state — count never updates in setInterval
Fix
Stale closure inside useEffect. Use the functional update form of setState: setCount(prev => prev + 1), or refactor with useRef to hold the latest value and reference that in the closure.
Symptom · 03
Memory grows monotonically in a long-running Node server
Fix
Use Chrome DevTools heap snapshot (or Node's --inspect). Look for closures under (closure) in the tree. Check for event listeners that are never removed or large arrays captured in closure scope that shouldn't be.
★ Quick Debug: Closure Memory Leaks & Stale ValuesThese are the two most critical closure problems in production. Here's how to diagnose and fix them fast.
DOM event handler closure keeps parent scope alive — memory not released after element removal
Immediate action
Run `performance.memory.usedJSHeapSize` in browser console. If it keeps growing after removing DOM elements, you've got a closure leak.
Commands
heap snapshot (DevTools Memory tab) — filter by '(closure)' and look for retained objects with unexpected large arrays.
`getEventListeners(window)` in console (Chrome-only) to list all event listeners. Check for any that shouldn't exist.
Fix now
Replace anonymous closure listeners with named functions, then remove them explicitly in cleanup. Or use { once: true } if the event should fire only once.
Stale closure in React useEffect — interval runs but uses old state+
Immediate action
Check the dependency array of useEffect. If you're using `[]` but referencing state, the closure captured the initial value.
Commands
Add a `console.log(state)` inside the effect — if it logs the same value every time, that's the captured stale value.
Use functional update for setState: `setCount(c => c + 1)` instead of `setCount(count + 1)`.
Fix now
Change [ ] to [state] or use useRef to hold a mutable reference that doesn't require the effect to re-run.
Closure Scoping: var vs let in Loops
Aspectvar in a for looplet in a for loop
ScopeFunction-scoped (one binding for the whole loop)Block-scoped (new binding per iteration)
Closure behaviourAll callbacks share the same variable referenceEach callback captures its own independent binding
Loop bug riskHigh — classic closure trapNone — spec guarantees per-iteration binding
When to useAlmost never in modern codeDefault choice for all loop variables
Fix required?Yes — IIFE, .forEach, or switch to letNo — works correctly out of the box
Memory overheadOne variable slot allocatedNew binding per iteration (negligible)

Key takeaways

1
A closure is a function plus its [[Environment]] slot
the live lexical bindings from where it was defined.
2
Closures capture variables by reference, never by value. This is the root of the 'loop bug'.
3
In V8, all inner functions from the same scope share a context record; one large variable can keep the entire scope alive.
4
Stale closures in React are solved using functional updates or useRef to maintain a stable pointer to current state.
5
Every event listener that uses a closure must be explicitly removed to prevent memory leaks.

Common mistakes to avoid

3 patterns
×

Thinking closures capture VALUES

Symptom
You expect a closure to remember the value of a variable at the time of creation, but it actually sees the latest value when executed.
Fix
Remember: closures capture variable BINDINGS (references), not values. If the variable is updated, the closure sees the update. Use an IIFE or let per iteration to capture a specific value.
×

Creating memory leaks with unremoved event listeners

Symptom
Memory grows over time, especially in SPAs after navigating pages. Heap snapshots show detached DOM trees.
Fix
Always remove event listeners with removeEventListener in component cleanup. Use { once: true } for one-shot events. In React, use the useEffect cleanup function.
×

Suppressing React's exhaustive-deps lint rule

Symptom
State in useEffect or useCallback appears stale — it always logs the initial value, not the current one.
Fix
Never suppress the lint rule. Either add the missing dependency or use functional state updates (setCount(prev => prev + 1)) to avoid capturing the state value.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the 'Diamond of Death' equivalent in JS Closures: if two functio...
Q02SENIOR
Why does 'use strict' change the behavior of closures in certain context...
Q03SENIOR
Describe the performance implications of creating closures inside a high...
Q04SENIOR
How does the V8 engine optimize closures to prevent memory bloating? (Di...
Q01 of 04SENIOR

Explain the 'Diamond of Death' equivalent in JS Closures: if two functions share a parent scope, can they access each other's private variables? Why/Why not?

ANSWER
No, they cannot directly access each other's private variables. Each function only has access to its own local scope and the shared parent scope. They share the same closure record (the parent's context object), but they cannot access each other's local variables because those are in separate function scopes. However, because they share the same context object, modifying a property on that object would affect the other function's view if that property is a shared reference (like an object or array). This is the diamond-like pattern: two children sharing a parent scope but not each other's private data.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Does every JavaScript function create a closure?
02
Can I manually 'destroy' a closure to free memory?
03
How do closures work with asynchronous code (Promises/Async-Await)?
04
What is the difference between a closure and a pure function?
N
Naren Founder & Principal Engineer

20+ years shipping production code across the stack, with years spent interviewing engineers. Written from production experience, not tutorials.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's JavaScript Interview. Mark it forged?

6 min read · try the examples if you haven't

Previous
Top 50 JavaScript Interview Q
2 / 5 · JavaScript Interview
Next
React Interview Questions