JavaScript Tutorial

JavaScript Execution Model: How JavaScript Code Actually Runs

Master JavaScript's execution model with deep dive into execution context, call stack, hoisting, scope chain, and the event loop. Learn how JavaScript runs your code behind the scenes.

Welcome back! šŸ‘‹ In the previous lesson, we learned what JavaScript is and where it runs. Now, let's dive deeper and understand how JavaScript actually executes your code behind the scenes.

Understanding the execution model is crucial because it helps you:

  • Write more efficient code
  • Debug issues faster
  • Avoid common pitfalls like hoisting bugs
  • Understand why certain errors occur
  • Master asynchronous programming

Let's pull back the curtain and see what happens when you run JavaScript code!


How JavaScript Code Executes: The Big Picture

When you run a JavaScript program, several things happen in a specific order:

  1. Code Parsing: JavaScript engine reads and validates your code
  2. Compilation: Modern engines compile code to optimized machine code (Just-In-Time compilation)
  3. Execution Context Creation: Environment is set up for code execution
  4. Code Execution: Your code runs line by line

Key Concept: JavaScript is single-threaded, meaning it can only execute one piece of code at a time. Think of it as a single worker handling tasks one by one.

console.log("First");
console.log("Second");
console.log("Third");

// Output (in order):
// First
// Second
// Third

Execution Context: The Environment Where Code Runs

Execution Context is the environment in which JavaScript code is executed. Think of it as a container that stores all the information needed to run your code.

Types of Execution Contexts

JavaScript creates different execution contexts:

  1. Global Execution Context (GEC)

    • Created when your script first runs
    • Only one per program
    • Creates the global object (window in browsers, global in Node.js)
    • Sets up the this keyword
  2. Function Execution Context (FEC)

    • Created every time a function is called
    • Each function call gets its own context
    • Destroyed after function completes
  3. Eval Execution Context (rarely used, avoid in practice)

    • Created for code inside eval() function

What's Inside an Execution Context?

Every execution context has two phases:

Phase 1: Creation Phase (Memory Creation)

Before any code runs, JavaScript sets up the environment:

Variable Environment:

  • Stores variables and function declarations
  • Variables declared with var are initialized with undefined
  • let and const are declared but not initialized (Temporal Dead Zone)
  • Function declarations are stored in their entirety

Scope Chain:

  • Reference to outer environment (parent scope)
  • Enables access to variables from outer scopes

this Keyword:

  • Determined based on how the function is called
// Example of Creation Phase

console.log(name); // undefined (not an error!)
console.log(age);  // ReferenceError: Cannot access 'age' before initialization

var name = "Mihir";
let age = 25;

greet(); // "Hello!" (works even before declaration!)

function greet() {
  console.log("Hello!");
}

Why does this happen?

  • var name is hoisted and initialized with undefined
  • let age is hoisted but NOT initialized (Temporal Dead Zone)
  • function greet() is fully hoisted with its body

Phase 2: Execution Phase

Now JavaScript runs your code line by line:

  • Assigns actual values to variables
  • Executes function calls
  • Evaluates expressions
var x = 10;        // Assignment happens
let y = 20;        // Assignment happens
const z = x + y;   // Expression evaluated

console.log(z);    // 30

Execution Context in Action

Let's trace through a complete example:

var globalVar = "I'm global";

function outer() {
  var outerVar = "I'm in outer";
  
  function inner() {
    var innerVar = "I'm in inner";
    console.log(globalVar); // Can access global
    console.log(outerVar);  // Can access outer
    console.log(innerVar);  // Can access inner
  }
  
  inner();
}

outer();

What happens:

  1. Global Execution Context created

    • globalVar declared and assigned "I'm global"
    • outer function stored in memory
  2. outer() is called → Function Execution Context created

    • outerVar declared and assigned "I'm in outer"
    • inner function stored in memory
  3. inner() is called → New Function Execution Context created

    • innerVar declared and assigned "I'm in inner"
    • All three console.log statements execute
    • Can access variables from all three scopes!
  4. inner() completes → Its context is destroyed

  5. outer() completes → Its context is destroyed

  6. Program ends → Global context remains until page/script ends


The Call Stack: JavaScript's Task Manager

The call stack is a data structure that tracks which function is currently executing and what functions are waiting to run.

How the Call Stack Works

Think of the call stack like a stack of plates:

  • You can only add plates to the top (push)
  • You can only remove plates from the top (pop)
  • Last plate added is the first one removed (LIFO - Last In, First Out)
function first() {
  console.log("Inside first");
  second();
  console.log("Back in first");
}

function second() {
  console.log("Inside second");
  third();
  console.log("Back in second");
}

function third() {
  console.log("Inside third");
}

first();

Call Stack Visualization:

Step 1: first() is called
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ first() │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Step 2: second() is called from first()
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ second() │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ first()  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Step 3: third() is called from second()
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ third() │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│second() │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│first()  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Step 4: third() completes and is popped
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ second() │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ first()  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Step 5: second() completes and is popped
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ first() │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Step 6: first() completes and is popped
(Stack is empty)

Output:

Inside first
Inside second
Inside third
Back in second
Back in first

Stack Overflow Error

The call stack has a limited size. If you create too many nested function calls, you'll get a stack overflow error.

function recursiveFunction() {
  recursiveFunction(); // Calls itself infinitely
}

recursiveFunction();
// RangeError: Maximum call stack size exceeded

Why it happens:

  • Each call adds to the stack
  • No base case to stop recursion
  • Stack runs out of memory

Fixed version with base case:

function countdown(n) {
  if (n <= 0) {
    console.log("Done!");
    return; // Base case - stops recursion
  }
  console.log(n);
  countdown(n - 1); // Recursive call
}

countdown(5);
// Output: 5, 4, 3, 2, 1, Done!

Hoisting: Why You Can Use Things Before Declaration

Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the creation phase.

Important: Only Declarations Are Hoisted, Not Initializations

// What you write:
console.log(name); // undefined
var name = "Mihir";

// How JavaScript interprets it:
var name;              // Declaration hoisted
console.log(name);     // undefined
name = "Mihir";        // Assignment stays in place

Hoisting with var, let, and const

var - Hoisted and Initialized:

console.log(x); // undefined (no error)
var x = 10;
console.log(x); // 10

let and const - Hoisted but NOT Initialized (Temporal Dead Zone):

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;

console.log(z); // ReferenceError: Cannot access 'z' before initialization
const z = 30;

Temporal Dead Zone (TDZ): The period between the start of the scope and the actual declaration where the variable exists but can't be accessed.

{
  // TDZ starts
  console.log(name); // ReferenceError
  // TDZ continues
  // TDZ continues
  let name = "Mihir"; // TDZ ends
  console.log(name);  // "Mihir" - works fine
}

Function Hoisting

Function Declarations - Fully Hoisted:

greet(); // "Hello!" - Works!

function greet() {
  console.log("Hello!");
}

Function Expressions - NOT Hoisted:

sayHi(); // TypeError: sayHi is not a function

var sayHi = function() {
  console.log("Hi!");
};

// Why? Because only the variable declaration is hoisted:
// var sayHi;           // undefined
// sayHi();             // undefined() - Error!
// sayHi = function...  // Assignment happens later

Arrow Functions - NOT Hoisted:

greeting(); // ReferenceError

const greeting = () => {
  console.log("Hey!");
};

Best Practices to Avoid Hoisting Issues

  1. Always declare variables at the top of their scope
function example() {
  let x = 10;  // Declare first
  let y = 20;
  let z = x + y;
  console.log(z);
}
  1. Use let and const instead of var
// Bad
var name = "Mihir";

// Good
const name = "Mihir"; // If value won't change
let age = 25;         // If value will change
  1. Define functions before calling them
// Good practice
function calculate() {
  return 42;
}

const result = calculate();

Scope and Scope Chain: Variable Access Rules

Scope determines where variables are accessible in your code.

Types of Scope

1. Global Scope

  • Variables declared outside any function
  • Accessible everywhere in your code
  • Avoid polluting global scope
const globalVar = "I'm global";

function test() {
  console.log(globalVar); // Accessible here
}

test();
console.log(globalVar); // Accessible here too

2. Function Scope (Local Scope)

  • Variables declared inside a function
  • Only accessible within that function
  • Created with var, let, or const
function myFunction() {
  var functionVar = "I'm local";
  console.log(functionVar); // Works
}

myFunction();
console.log(functionVar); // ReferenceError: functionVar is not defined

3. Block Scope

  • Variables declared inside { } blocks
  • Only let and const are block-scoped
  • var is NOT block-scoped
if (true) {
  var x = 10;    // Function-scoped
  let y = 20;    // Block-scoped
  const z = 30;  // Block-scoped
}

console.log(x); // 10 - accessible (var ignores block)
console.log(y); // ReferenceError (let respects block)
console.log(z); // ReferenceError (const respects block)

4. Lexical Scope (Static Scope)

  • Inner functions have access to outer function variables
  • Determined by where functions are written, not where they're called
function outer() {
  const outerVar = "outer";
  
  function inner() {
    const innerVar = "inner";
    console.log(outerVar); // Can access outer variable
    console.log(innerVar); // Can access own variable
  }
  
  inner();
  console.log(innerVar); // ReferenceError - can't access inner variable
}

outer();

The Scope Chain

When JavaScript looks for a variable, it follows the scope chain:

  1. Look in the current scope
  2. If not found, look in the parent scope
  3. Keep going up until reaching global scope
  4. If still not found, throw a ReferenceError
const global = "global";

function level1() {
  const level1Var = "level1";
  
  function level2() {
    const level2Var = "level2";
    
    function level3() {
      const level3Var = "level3";
      
      console.log(level3Var); // Found in level3
      console.log(level2Var); // Found in level2 (parent)
      console.log(level1Var); // Found in level1 (grandparent)
      console.log(global);    // Found in global scope
    }
    
    level3();
  }
  
  level2();
}

level1();

Scope Chain Visualization:

level3() → level2() → level1() → Global Scope

Variable Shadowing

When inner scope declares a variable with the same name as an outer variable:

const name = "Global Mihir";

function test() {
  const name = "Local Mihir"; // Shadows global name
  console.log(name); // "Local Mihir"
}

test();
console.log(name); // "Global Mihir"

The Event Loop: Handling Asynchronous Code

JavaScript is single-threaded but can handle asynchronous operations thanks to the event loop.

The Problem: Blocking Code

console.log("Start");

// This would block for 3 seconds
// (if we had a blocking sleep function)
sleep(3000);

console.log("End");

If JavaScript were purely synchronous, the browser would freeze for 3 seconds. Not good!

The Solution: Asynchronous Execution

JavaScript uses:

  • Call Stack: For synchronous code
  • Web APIs: Browser-provided features (timers, HTTP requests, DOM events)
  • Callback Queue: Holds callbacks ready to execute
  • Event Loop: Checks if call stack is empty and moves callbacks from queue to stack

How the Event Loop Works

Components:

  1. Call Stack: Executes code
  2. Web APIs: Handle async operations (setTimeout, fetch, DOM events)
  3. Callback Queue (Task Queue): Stores callbacks from completed async operations
  4. Microtask Queue: Higher priority queue for Promises
  5. Event Loop: Continuously checks and moves tasks
console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise");
});

console.log("End");

Output:

Start
End
Promise
Timeout

Why this order?

Step-by-step execution:

  1. console.log("Start") → Call Stack → Output: "Start"
  2. setTimeout() → Sent to Web API → Callback registered
  3. Promise.resolve() → Microtask Queue → Callback queued
  4. console.log("End") → Call Stack → Output: "End"
  5. Call Stack is empty → Event Loop checks queues
  6. Microtask Queue has priority → Promise callback executes → Output: "Promise"
  7. Callback Queue → setTimeout callback executes → Output: "Timeout"

Visual Representation

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   Call Stack    │  ← Executes code
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         ↑
         │
    Event Loop (checks if empty)
         │
         ↓
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Microtask Queue │  ← Promises (higher priority)
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Callback Queue  │  ← setTimeout, setInterval, DOM events
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│    Web APIs     │  ← Browser features (timers, fetch, etc.)
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Real-World Example: setTimeout

console.log("1");

setTimeout(() => {
  console.log("2");
}, 1000);

console.log("3");

// Output:
// 1
// 3
// (1 second pause)
// 2

What happens:

  1. "1" logs immediately
  2. setTimeout sends callback to Web API (starts 1-second timer)
  3. "3" logs immediately (doesn't wait for timer)
  4. After 1 second, callback moves to Callback Queue
  5. Event Loop moves callback to Call Stack
  6. "2" logs

Why setTimeout(fn, 0) Doesn't Run Immediately

console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

console.log("C");

// Output: A, C, B (not A, B, C!)

Even with 0ms delay, the callback goes through the event loop:

  1. Synchronous code runs first (A, C)
  2. Then event loop processes async callbacks (B)

Putting It All Together: Complete Example

Let's trace through a complex example:

const name = "Mihir";

function first() {
  console.log("First function start");
  second();
  console.log("First function end");
}

function second() {
  console.log("Second function start");
  
  setTimeout(() => {
    console.log("Timeout callback");
  }, 0);
  
  Promise.resolve().then(() => {
    console.log("Promise callback");
  });
  
  console.log("Second function end");
}

console.log("Script start");
first();
console.log("Script end");

Output:

Script start
First function start
Second function start
Second function end
First function end
Script end
Promise callback
Timeout callback

Detailed Execution Trace:

  1. Global Execution Context created
  2. "Script start" → logged
  3. first() called → Function Context created → pushed to Call Stack
  4. "First function start" → logged
  5. second() called → New Function Context → pushed to Call Stack
  6. "Second function start" → logged
  7. setTimeout() → sent to Web API
  8. Promise.resolve() → callback to Microtask Queue
  9. "Second function end" → logged
  10. second() completes → popped from Call Stack
  11. "First function end" → logged
  12. first() completes → popped from Call Stack
  13. "Script end" → logged
  14. Call Stack empty → Event Loop checks queues
  15. Microtask Queue: "Promise callback" → logged
  16. Callback Queue: "Timeout callback" → logged

Key Takeaways

Understanding JavaScript's execution model helps you:

āœ… Execution Context

  • Created in two phases: Creation (memory setup) and Execution (code runs)
  • Variables, functions, and this are set up before code executes

āœ… Call Stack

  • Tracks function execution using LIFO (Last In, First Out)
  • Stack overflow occurs with infinite recursion

āœ… Hoisting

  • Declarations are moved to the top during creation phase
  • var is initialized with undefined, let/const remain uninitialized (TDZ)
  • Function declarations are fully hoisted

āœ… Scope and Scope Chain

  • Global, function, and block scope determine variable accessibility
  • JavaScript searches through scope chain to find variables
  • Lexical scoping: inner functions access outer variables

āœ… Event Loop

  • Enables asynchronous programming in single-threaded JavaScript
  • Microtask Queue (Promises) has higher priority than Callback Queue (setTimeout)
  • Event loop moves callbacks to call stack when it's empty

Practice Questions

Test your understanding with these questions:

Question 1

console.log(x);
var x = 5;

What will be logged and why?

Answer: undefined will be logged because var x is hoisted and initialized with undefined during the creation phase, but the assignment x = 5 happens later during the execution phase.

Question 2

function outer() {
  var a = 10;
  function inner() {
    console.log(a);
  }
  return inner;
}

const fn = outer();
fn();

What will be logged and why?

Answer: 10 will be logged. The inner function has access to outer's variable a through closure and the scope chain, even after outer() has finished executing. This is because the inner function maintains a reference to its parent scope.

Question 3

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");

What is the output order?

Answer: The output will be: A, D, C, B

Explanation:

  • Synchronous code executes first: A, D
  • Then the Microtask Queue (Promises) executes: C
  • Finally the Callback Queue (setTimeout) executes: B

Even though setTimeout has 0ms delay, it still goes through the event loop and has lower priority than Promises.


What's Next?

Congratulations! šŸŽ‰ You now understand how JavaScript executes code behind the scenes.

In the next lesson, we'll set up your development environment by installing Node.js. You'll learn:

  • What Node.js is and why developers use it
  • How to install Node.js on your system
  • Understanding npm (Node Package Manager)
  • Running JavaScript files outside the browser
  • Setting up a professional development workflow

This is an essential step before we dive into JavaScript fundamentals!