Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/a2836c37acf966f9db442b56223195ee to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/a2836c37acf966f9db442b56223195ee to your computer and use it in GitHub Desktop.
JavaScript Execution Model: From Start to Finish

JavaScript Execution Model: From Start to Finish

Table of Contents

  1. JavaScript Execution Overview
  2. Single-Threaded Nature
  3. JavaScript Engine
  4. Execution Context
  5. Memory Heap
  6. Call Stack
  7. Phases of Execution
  8. Hoisting
  9. Scope and Scope Chain
  10. Lexical Environment
  11. Event Loop
  12. Web APIs
  13. Asynchronous JavaScript
  14. Complete Execution Flow Example
  15. Visualization Summary

JavaScript Execution Overview

JavaScript is a single-threaded, synchronous, blocking programming language with asynchronous capabilities provided by the browser or Node.js runtime environment. Understanding how JavaScript executes code from start to finish is crucial for writing efficient and bug-free code.

Key Points:

  • JavaScript can only execute one piece of code at a time
  • Code executes line by line in a synchronous manner
  • Asynchronous operations are handled by the runtime environment
  • The event loop enables non-blocking asynchronous behavior

Single-Threaded Nature

JavaScript has a single call stack, which means it can only do one thing at a time. This is what makes JavaScript single-threaded.

What does single-threaded mean?

  • Only one call stack
  • Only one piece of code executes at any given time
  • Code runs sequentially, one operation after another
  • Blocking operations can freeze the entire application

Example:

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

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

Why Single-Threaded?

  • Simplicity: No race conditions or deadlocks
  • Originally designed for simple browser interactions
  • Easier to reason about code execution flow

Challenges:

  • Long-running operations block the main thread
  • Can cause UI freezing in browsers
  • Requires asynchronous patterns for I/O operations

JavaScript Engine

The JavaScript engine is a program that executes JavaScript code. It reads the code, compiles it, and runs it.

Components of JS Engine

  1. Memory Heap: Where memory allocation happens for variables and objects
  2. Call Stack: Keeps track of function calls and execution contexts
  3. Garbage Collector: Automatically frees up memory that's no longer needed

Popular JavaScript Engines

  • V8: Chrome, Edge, Node.js (developed by Google)
  • SpiderMonkey: Firefox (developed by Mozilla)
  • JavaScriptCore (Nitro): Safari (developed by Apple)
  • Chakra: Old Edge (developed by Microsoft)

Engine Process:

Source Code → Parsing → Abstract Syntax Tree (AST) 
→ Compilation (JIT) → Machine Code → Execution

Execution Context

An execution context is an abstract concept that represents the environment in which JavaScript code is evaluated and executed. Think of it as a container that holds all the necessary information for code execution.

Types of Execution Context

  1. Global Execution Context (GEC)

    • Created when JavaScript starts executing
    • Only one per program
    • Creates global object (window in browsers, global in Node.js)
    • Sets this to the global object
  2. Function Execution Context (FEC)

    • Created whenever a function is invoked
    • Can have multiple function execution contexts
    • Each function call creates a new context
  3. Eval Execution Context

    • Created when code runs inside eval() function
    • Rarely used and not recommended

Execution Context Stack (Call Stack)

The execution context stack (call stack) manages the execution contexts in a Last-In-First-Out (LIFO) order.

function first() {
  console.log('Inside first');
  second();
  console.log('Back to first');
}

function second() {
  console.log('Inside second');
}

console.log('Global');
first();
console.log('Back to Global');

// Call Stack visualization:
// 1. Global Execution Context (bottom)
// 2. Global EC → first() EC
// 3. Global EC → first() EC → second() EC
// 4. Global EC → first() EC (second completed)
// 5. Global EC (first completed)

Components of Execution Context

Each execution context has three main components:

  1. Variable Environment

    • Storage for variables, functions, and arguments
    • var declarations stored here
  2. Lexical Environment

    • Structure to hold identifier-variable mapping
    • let and const declarations stored here
    • Reference to outer environment (scope chain)
  3. This Binding

    • Value of this keyword
// Execution Context structure
ExecutionContext = {
  VariableEnvironment: {
    // var declarations
    // function declarations
  },
  LexicalEnvironment: {
    // let and const declarations
    // reference to outer environment
  },
  ThisBinding: {
    // value of 'this'
  }
}

Memory Heap

The memory heap is where objects and variables are stored in memory. It's an unstructured memory pool where JavaScript allocates memory for:

  • Objects
  • Arrays
  • Functions
  • Closures

Characteristics:

  • Unordered memory allocation
  • Dynamic memory allocation
  • Managed by garbage collector
  • Can cause memory leaks if not managed properly
// Memory allocation in heap
let user = {
  name: 'Alice',
  age: 30
};

let numbers = [1, 2, 3, 4, 5];

// These objects are stored in the heap
// Variables (user, numbers) are references stored in stack

Call Stack

The call stack is a data structure that keeps track of function calls in a Last-In-First-Out (LIFO) manner. It records where in the program we are.

How Call Stack Works:

  1. When script starts, global execution context is pushed to stack
  2. When function is called, its execution context is pushed
  3. When function completes, its context is popped off
  4. Process continues until stack is empty

Example:

function multiply(a, b) {
  return a * b;
}

function square(n) {
  return multiply(n, n);
}

function printSquare(n) {
  const result = square(n);
  console.log(result);
}

printSquare(4);

// Call Stack Flow:
// 1. [Global]
// 2. [Global, printSquare]
// 3. [Global, printSquare, square]
// 4. [Global, printSquare, square, multiply]
// 5. [Global, printSquare, square] (multiply returns)
// 6. [Global, printSquare] (square returns)
// 7. [Global] (printSquare returns)

Stack Overflow:

function recursiveFunction() {
  recursiveFunction(); // No base case
}

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

Phases of Execution

JavaScript executes code in two phases for each execution context:

Creation Phase

During this phase, JavaScript:

  1. Creates the scope chain
  2. Creates the Variable Object (contains arguments, function declarations, and variable declarations)
  3. Determines the value of this

What happens in creation phase:

  • Function declarations are hoisted and stored in memory
  • Variable declarations (var) are hoisted and initialized with undefined
  • let and const declarations are hoisted but not initialized (Temporal Dead Zone)
  • this binding is established
console.log(myVar); // undefined (hoisted)
console.log(myLet); // ReferenceError (TDZ)

var myVar = 'Hello';
let myLet = 'World';

// During creation phase:
// myVar is created and set to undefined
// myLet is created but not initialized

Execution Phase

During this phase, JavaScript:

  1. Assigns values to variables
  2. Executes the code line by line
  3. Handles function calls by creating new execution contexts
var x = 10;
let y = 20;

function add(a, b) {
  var result = a + b;
  return result;
}

const sum = add(x, y);
console.log(sum); // 30

// Execution Phase:
// 1. x is assigned 10
// 2. y is assigned 20
// 3. add function is already in memory
// 4. add is called, new execution context created
// 5. result is assigned 30
// 6. 30 is returned
// 7. sum is assigned 30
// 8. 30 is logged

Hoisting

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

What gets hoisted:

  • Function declarations (fully hoisted)
  • var declarations (hoisted and initialized with undefined)
  • let and const declarations (hoisted but not initialized - TDZ)

Examples:

// Function hoisting
sayHello(); // Works fine
function sayHello() {
  console.log('Hello!');
}

// Var hoisting
console.log(age); // undefined (not ReferenceError)
var age = 25;
console.log(age); // 25

// Let/Const - Temporal Dead Zone (TDZ)
console.log(name); // ReferenceError
let name = 'Alice';

// Function expressions are NOT hoisted
greet(); // TypeError: greet is not a function
var greet = function() {
  console.log('Hi!');
};

// How JavaScript sees the code:
var age;
var greet;
console.log(age); // undefined
age = 25;
greet(); // greet is undefined at this point
greet = function() {
  console.log('Hi!');
};

Temporal Dead Zone (TDZ):

// TDZ starts here for 'x'
console.log(x); // ReferenceError
let x = 10; // TDZ ends here

// Another example
function example() {
  // TDZ for 'y' starts here
  console.log(y); // ReferenceError
  let y = 20; // TDZ ends here
}

Scope and Scope Chain

Scope determines the accessibility of variables, functions, and objects in different parts of your code.

Types of Scope:

  1. Global Scope: Variables accessible everywhere
  2. Function Scope: Variables accessible only within function
  3. Block Scope: Variables accessible only within block (let/const)

Scope Chain: When a variable is used, JavaScript looks for it in the current scope, then moves up through parent scopes until found or reaching global scope.

const globalVar = 'Global';

function outer() {
  const outerVar = 'Outer';
  
  function inner() {
    const innerVar = 'Inner';
    
    console.log(innerVar);  // Found in inner scope
    console.log(outerVar);  // Found in outer scope (scope chain)
    console.log(globalVar); // Found in global scope (scope chain)
  }
  
  inner();
  console.log(innerVar); // ReferenceError: not in scope
}

outer();

// Scope Chain: inner → outer → global

Block Scope (let/const):

if (true) {
  var x = 10;  // Function scoped
  let y = 20;  // Block scoped
  const z = 30; // Block scoped
}

console.log(x); // 10 (accessible)
console.log(y); // ReferenceError
console.log(z); // ReferenceError

Lexical Environment

A lexical environment is a structure that holds identifier-variable mapping and has a reference to its outer lexical environment (parent scope). It's created every time an execution context is created.

Components:

  1. Environment Record: Stores variables and function declarations
  2. Reference to outer environment: Link to parent lexical environment
let globalVar = 'global';

function outerFunction() {
  let outerVar = 'outer';
  
  function innerFunction() {
    let innerVar = 'inner';
    console.log(innerVar);   // Own environment
    console.log(outerVar);   // Outer environment
    console.log(globalVar);  // Global environment
  }
  
  innerFunction();
}

outerFunction();

// Lexical Environment Chain:
// innerFunction LE → outerFunction LE → Global LE → null

Event Loop

The event loop is what allows JavaScript to perform non-blocking operations despite being single-threaded. It constantly checks if the call stack is empty and moves tasks from queues to the call stack.

How Event Loop Works

┌───────────────────────────┐
│    Call Stack (Empty?)    │
└───────────────────────────┘
            │
            ↓
┌───────────────────────────┐
│  Check Microtask Queue    │
│  (Promise callbacks, etc) │
└───────────────────────────┘
            │
            ↓
┌───────────────────────────┐
│   Check Task Queue        │
│   (setTimeout, events)    │
└───────────────────────────┘
            │
            ↓
┌───────────────────────────┐
│    Render (if needed)     │
└───────────────────────────┘
            │
            └──────→ Repeat

Event Loop Algorithm:

  1. Execute all code in call stack
  2. When call stack is empty, check microtask queue
  3. Execute all microtasks
  4. If call stack still empty, take one task from task queue
  5. Execute that task
  6. Repeat from step 2

Task Queue (Macrotask Queue)

The task queue (also called macrotask queue or callback queue) holds:

  • setTimeout callbacks
  • setInterval callbacks
  • setImmediate (Node.js)
  • I/O operations
  • UI rendering
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

console.log('3');

// Output:
// 1
// 3
// 2

// Explanation:
// 1. '1' logged (call stack)
// 2. setTimeout callback moved to task queue
// 3. '3' logged (call stack)
// 4. Call stack empty, event loop checks queues
// 5. Callback from task queue moved to call stack
// 6. '2' logged

Microtask Queue

The microtask queue has higher priority than the task queue and holds:

  • Promise callbacks (.then, .catch, .finally)
  • queueMicrotask()
  • MutationObserver callbacks
  • process.nextTick() (Node.js - even higher priority)
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

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

console.log('4');

// Output:
// 1
// 4
// 3
// 2

// Explanation:
// 1. '1' logged (synchronous)
// 2. setTimeout callback → task queue
// 3. Promise callback → microtask queue
// 4. '4' logged (synchronous)
// 5. Call stack empty
// 6. Microtask queue checked first → '3' logged
// 7. Task queue checked → '2' logged

Priority Order:

1. Synchronous code (call stack)
2. Microtasks (Promise callbacks)
3. Macrotasks (setTimeout, setInterval)

Web APIs

Web APIs are provided by the browser (or Node.js runtime) and are NOT part of JavaScript itself. They handle asynchronous operations and communicate with the JavaScript engine via the event loop.

Common Web APIs:

  • setTimeout / setInterval
  • fetch / XMLHttpRequest
  • DOM APIs (addEventListener, etc.)
  • localStorage / sessionStorage
  • console (console.log, etc.)
  • Promise (constructor)
// setTimeout is a Web API, not part of JS engine
setTimeout(() => {
  console.log('This runs after 2 seconds');
}, 2000);

// fetch is a Web API
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data));

// DOM API
document.getElementById('btn').addEventListener('click', () => {
  console.log('Button clicked!');
});

How Web APIs work with Event Loop:

JavaScript Engine          Web APIs          Event Loop
     │                        │                   │
     │──setTimeout()──────────>│                   │
     │                        │                   │
     │                  (Timer runs)              │
     │                        │                   │
     │                        │──Callback─────────>│
     │                        │               Task Queue
     │<────────────────────────────────────────────│
     │                                         (when stack empty)

Asynchronous JavaScript

JavaScript handles asynchronous operations through callbacks, promises, and async/await, all orchestrated by the event loop.

1. Callbacks:

function fetchData(callback) {
  setTimeout(() => {
    callback('Data received');
  }, 1000);
}

fetchData((data) => {
  console.log(data);
});

// Callback Hell problem:
getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      getMoreData(c, function(d) {
        console.log(d);
      });
    });
  });
});

2. Promises:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data received');
    }, 1000);
  });
}

fetchData()
  .then(data => console.log(data))
  .catch(error => console.error(error));

// Promise chaining
fetch('/api/user')
  .then(response => response.json())
  .then(user => fetch(`/api/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

3. Async/Await:

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

fetchData();

// Multiple async operations
async function getCompleteUserData(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchUserPosts(userId);
  const comments = await fetchUserComments(userId);
  
  return { user, posts, comments };
}

Execution Flow:

console.log('Start');

setTimeout(() => console.log('Timeout 1'), 0);

Promise.resolve()
  .then(() => console.log('Promise 1'))
  .then(() => console.log('Promise 2'));

setTimeout(() => console.log('Timeout 2'), 0);

Promise.resolve().then(() => console.log('Promise 3'));

console.log('End');

// Output:
// Start
// End
// Promise 1
// Promise 2
// Promise 3
// Timeout 1
// Timeout 2

Complete Execution Flow Example

Let's trace through a complete example to understand the entire execution process:

console.log('1: Start');

setTimeout(() => {
  console.log('2: Timeout 1');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise 1');
  setTimeout(() => {
    console.log('4: Timeout 2');
  }, 0);
});

setTimeout(() => {
  console.log('5: Timeout 3');
  Promise.resolve().then(() => {
    console.log('6: Promise 2');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('7: Promise 3');
});

console.log('8: End');

// Output:
// 1: Start
// 8: End
// 3: Promise 1
// 7: Promise 3
// 2: Timeout 1
// 5: Timeout 3
// 6: Promise 2
// 4: Timeout 2

Step-by-Step Execution:

1. Global Execution Context created
2. Call Stack: [Global]

3. console.log('1: Start') executes → Output: "1: Start"

4. setTimeout (Timeout 1) → Web API handles it → After 0ms → Task Queue

5. Promise.resolve().then → Microtask Queue

6. setTimeout (Timeout 3) → Web API handles it → After 0ms → Task Queue

7. Promise.resolve().then → Microtask Queue

8. console.log('8: End') executes → Output: "8: End"

9. Call Stack empty → Check Microtask Queue

10. Execute Promise 1 callback:
    - Output: "3: Promise 1"
    - setTimeout (Timeout 2) → Task Queue

11. Execute Promise 3 callback:
    - Output: "7: Promise 3"

12. Microtask Queue empty → Check Task Queue

13. Execute Timeout 1 callback:
    - Output: "2: Timeout 1"

14. Microtask Queue empty → Check Task Queue

15. Execute Timeout 3 callback:
    - Output: "5: Timeout 3"
    - Promise.resolve().then → Microtask Queue

16. Check Microtask Queue (higher priority)

17. Execute Promise 2 callback:
    - Output: "6: Promise 2"

18. Microtask Queue empty → Check Task Queue

19. Execute Timeout 2 callback:
    - Output: "4: Timeout 2"

20. All queues empty → Program complete

Visualization Summary

Complete JavaScript Runtime Architecture:

┌─────────────────────────────────────────────────┐
│            JavaScript Engine (V8)               │
│                                                 │
│  ┌──────────────┐         ┌─────────────────┐  │
│  │  Call Stack  │         │   Memory Heap   │  │
│  │              │         │                 │  │
│  │  [Function]  │         │  Objects, etc.  │  │
│  │  [Function]  │         │                 │  │
│  │   [Global]   │         │                 │  │
│  └──────────────┘         └─────────────────┘  │
└─────────────────────────────────────────────────┘
                    ↕
┌─────────────────────────────────────────────────┐
│              Web APIs (Browser)                 │
│                                                 │
│  setTimeout/setInterval, fetch, DOM APIs, etc.  │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│              Event Loop                         │
│                                                 │
│  ┌─────────────────────┐  ┌──────────────────┐ │
│  │  Microtask Queue    │  │   Task Queue     │ │
│  │  (Promises)         │  │   (setTimeout)   │ │
│  │  Higher Priority    │  │                  │ │
│  └─────────────────────┘  └──────────────────┘ │
└─────────────────────────────────────────────────┘

Execution Order:

1. Synchronous Code (Call Stack)
   ↓
2. Microtasks (Promise callbacks)
   ↓
3. Render (if needed)
   ↓
4. Macrotasks (setTimeout, setInterval)
   ↓
5. Repeat from step 2

Key Takeaways:

  1. JavaScript is single-threaded but can handle asynchronous operations
  2. Call stack manages function execution in LIFO order
  3. Memory heap stores objects and variables
  4. Execution contexts are created in two phases: creation and execution
  5. Hoisting moves declarations to the top of their scope
  6. Scope chain determines variable accessibility
  7. Event loop enables asynchronous behavior by managing queues
  8. Microtasks have priority over macrotasks
  9. Web APIs handle async operations outside the JS engine
  10. Understanding execution flow is crucial for debugging and optimization

Additional Resources

  • Call Stack Visualization: Use browser DevTools to see call stack
  • Event Loop Visualization: latentflip.com/loupe
  • Performance Profiling: Chrome DevTools Performance tab
  • Memory Profiling: Chrome DevTools Memory tab

Understanding JavaScript's execution model helps you:

  • Write more efficient code
  • Debug complex issues
  • Avoid common pitfalls
  • Optimize performance
  • Master asynchronous programming
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment