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:

  1. Global Execution Context — created when the script first runs. Sets up the global object (window/globalThis) and this binding.
  2. Function Execution Context — created each time a function is invoked.
  3. Eval Execution Context — created inside eval() (avoid in production).

Each execution context has two phases:

  • Creation Phase: JavaScript engine scans for declarations. var declarations are hoisted and initialized to undefined. let/const declarations 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:

  1. 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

codeTap to expand ⛶
1// === EXECUTION CONTEXT VISUALIZATION ===
2// Call Stack progression:
3
4var x = 10; // Global EC created
5
6function outer(a) { // outer EC created when called
7 var y = 20;
8
9 function inner(b) { // inner EC created when called
10 var z = 30;
11 // Scope chain: inner → outer → global
12 console.log(a + b + x + y + z); // Can access all
13 }
14
15 inner(5); // inner EC pushed, executed, popped
16}
17
18outer(1); // outer EC pushed → inner EC pushed/popped → outer EC popped
19
20// === CLOSURE: REAL PRODUCTION USE CASES ===
21
22// 1. React Native: Stale closure in useEffect
23function ChatScreen() {
24 const [messages, setMessages] = useState([]);
25
26 // ❌ BUG: Stale closure — captures initial empty array
27 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 once
35
36 // ✅ FIX: Use functional update to access latest state
37 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}
45
46// 2. Debounce implementation using closures
47function debounce<T extends (...args: any[]) => any>(
48 fn: T,
49 delay: number
50): (...args: Parameters<T>) => void {
51 let timeoutId: ReturnType<typeof setTimeout> | null = null;
52
53 // Returned function closes over timeoutId and fn
54 return (...args: Parameters<T>) => {
55 if (timeoutId) clearTimeout(timeoutId);
56 timeoutId = setTimeout(() => {
57 fn(...args);
58 timeoutId = null;
59 }, delay);
60 };
61}
62
63// 3. Module pattern — private state via closure
64function createAPIClient(baseURL: string) {
65 let authToken: string | null = null; // Private — only accessible via closure
66 let requestCount = 0; // Private counter
67
68 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}
83
84// authToken is truly private — no way to access it except through setToken
85const api = createAPIClient('https://api.example.com');
86api.setToken('secret123');

🏋️ Practice Exercise

Deep Practice:

  1. Draw the call stack and scope chain for a 3-level nested function call
  2. Implement a once(fn) function using closures that ensures fn is called at most once
  3. Implement a memoize(fn) function that caches results based on arguments
  4. 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);
    }, []);
    
  5. Explain why let fixes the classic setTimeout in a loop problem but var doesn't
  6. 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.