Execution Context, Closures & Scope Chain
📖 Concept
Execution Context is the environment in which JavaScript code is evaluated and executed. Every time a function is called, a new execution context is created and pushed onto the call stack.
Three types of execution contexts:
- Global Execution Context — created when the script first runs. Sets up the global object (
window/globalThis) andthisbinding. - Function Execution Context — created each time a function is invoked.
- Eval Execution Context — created inside
eval()(avoid in production).
Each execution context has two phases:
- Creation Phase: JavaScript engine scans for declarations.
vardeclarations are hoisted and initialized toundefined.let/constdeclarations are hoisted but NOT initialized (Temporal Dead Zone). Function declarations are hoisted entirely. - Execution Phase: Code runs line by line, assignments happen, functions execute.
Scope Chain is the mechanism by which JavaScript resolves variable references. Each execution context has a reference to its outer environment. When a variable is referenced, the engine searches:
- Current scope → 2. Parent scope → 3. ... → n. Global scope → ReferenceError
Closures occur when a function retains access to its lexical scope even after the outer function has returned. This is NOT just an academic concept — closures are the foundation of:
- React hooks (useState, useEffect internally use closures)
- Event handlers retaining state
- Module patterns and data privacy
- Debounce/throttle implementations
- Memoization caches
Production-critical closure behavior in React Native:
Stale closures are the #1 source of subtle bugs in hooks. When a useEffect or useCallback captures a value, it captures the value at the time of the closure creation, not a live reference.
💻 Code Example
1// === EXECUTION CONTEXT VISUALIZATION ===2// Call Stack progression:34var x = 10; // Global EC created56function outer(a) { // outer EC created when called7 var y = 20;89 function inner(b) { // inner EC created when called10 var z = 30;11 // Scope chain: inner → outer → global12 console.log(a + b + x + y + z); // Can access all13 }1415 inner(5); // inner EC pushed, executed, popped16}1718outer(1); // outer EC pushed → inner EC pushed/popped → outer EC popped1920// === CLOSURE: REAL PRODUCTION USE CASES ===2122// 1. React Native: Stale closure in useEffect23function ChatScreen() {24 const [messages, setMessages] = useState([]);2526 // ❌ BUG: Stale closure — captures initial empty array27 useEffect(() => {28 const ws = new WebSocket('wss://api.example.com/chat');29 ws.onmessage = (event) => {30 // 'messages' is ALWAYS [] here — stale closure!31 setMessages([...messages, JSON.parse(event.data)]);32 };33 return () => ws.close();34 }, []); // Empty deps = closure created once3536 // ✅ FIX: Use functional update to access latest state37 useEffect(() => {38 const ws = new WebSocket('wss://api.example.com/chat');39 ws.onmessage = (event) => {40 setMessages(prev => [...prev, JSON.parse(event.data)]);41 };42 return () => ws.close();43 }, []);44}4546// 2. Debounce implementation using closures47function debounce<T extends (...args: any[]) => any>(48 fn: T,49 delay: number50): (...args: Parameters<T>) => void {51 let timeoutId: ReturnType<typeof setTimeout> | null = null;5253 // Returned function closes over timeoutId and fn54 return (...args: Parameters<T>) => {55 if (timeoutId) clearTimeout(timeoutId);56 timeoutId = setTimeout(() => {57 fn(...args);58 timeoutId = null;59 }, delay);60 };61}6263// 3. Module pattern — private state via closure64function createAPIClient(baseURL: string) {65 let authToken: string | null = null; // Private — only accessible via closure66 let requestCount = 0; // Private counter6768 return {69 setToken(token: string) { authToken = token; },70 getRequestCount() { return requestCount; },71 async fetch(endpoint: string, options?: RequestInit) {72 requestCount++;73 return fetch(`${baseURL}${endpoint}`, {74 ...options,75 headers: {76 ...options?.headers,77 ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),78 },79 });80 },81 };82}8384// authToken is truly private — no way to access it except through setToken85const api = createAPIClient('https://api.example.com');86api.setToken('secret123');
🏋️ Practice Exercise
Deep Practice:
- Draw the call stack and scope chain for a 3-level nested function call
- Implement a
once(fn)function using closures that ensuresfnis called at most once - Implement a
memoize(fn)function that caches results based on arguments - Find and fix the stale closure bug in this React Native code:
const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(() => { setCount(count + 1); // Bug: always sets to 1 }, 1000); return () => clearInterval(interval); }, []); - Explain why
letfixes the classicsetTimeoutin a loop problem butvardoesn't - Build a rate limiter using closures that allows at most N calls per T milliseconds
⚠️ Common Mistakes
Stale closures in React hooks — capturing a value inside useEffect/useCallback without proper dependency tracking
Memory leaks from closures holding references to large objects — the GC can't collect anything the closure references
Confusing block scope (let/const) with function scope (var) when reasoning about closures
Not understanding that closures capture variables by reference, not by value — the value at read time matters, not at closure creation
Creating unnecessary closures in hot paths (render functions, FlatList item renderers) causing GC pressure
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Execution Context, Closures & Scope Chain. Login to unlock this feature.