- JavaScript Execution Overview
- Single-Threaded Nature
- JavaScript Engine
- Execution Context
- Memory Heap
- Call Stack
- Phases of Execution
- Hoisting
- Scope and Scope Chain
- Lexical Environment
- Event Loop
- Web APIs
- Asynchronous JavaScript
- Complete Execution Flow Example
- Visualization Summary
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
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
// ThirdWhy 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
The JavaScript engine is a program that executes JavaScript code. It reads the code, compiles it, and runs it.
- Memory Heap: Where memory allocation happens for variables and objects
- Call Stack: Keeps track of function calls and execution contexts
- Garbage Collector: Automatically frees up memory that's no longer needed
- 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
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.
-
Global Execution Context (GEC)
- Created when JavaScript starts executing
- Only one per program
- Creates global object (
windowin browsers,globalin Node.js) - Sets
thisto the global object
-
Function Execution Context (FEC)
- Created whenever a function is invoked
- Can have multiple function execution contexts
- Each function call creates a new context
-
Eval Execution Context
- Created when code runs inside
eval()function - Rarely used and not recommended
- Created when code runs inside
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)Each execution context has three main components:
-
Variable Environment
- Storage for variables, functions, and arguments
vardeclarations stored here
-
Lexical Environment
- Structure to hold identifier-variable mapping
letandconstdeclarations stored here- Reference to outer environment (scope chain)
-
This Binding
- Value of
thiskeyword
- Value of
// Execution Context structure
ExecutionContext = {
VariableEnvironment: {
// var declarations
// function declarations
},
LexicalEnvironment: {
// let and const declarations
// reference to outer environment
},
ThisBinding: {
// value of 'this'
}
}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 stackThe 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:
- When script starts, global execution context is pushed to stack
- When function is called, its execution context is pushed
- When function completes, its context is popped off
- 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 exceededJavaScript executes code in two phases for each execution context:
During this phase, JavaScript:
- Creates the scope chain
- Creates the Variable Object (contains arguments, function declarations, and variable declarations)
- 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 withundefined letandconstdeclarations are hoisted but not initialized (Temporal Dead Zone)thisbinding 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 initializedDuring this phase, JavaScript:
- Assigns values to variables
- Executes the code line by line
- 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 loggedHoisting is JavaScript's behavior of moving declarations to the top of their scope during the creation phase.
What gets hoisted:
- Function declarations (fully hoisted)
vardeclarations (hoisted and initialized withundefined)letandconstdeclarations (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 determines the accessibility of variables, functions, and objects in different parts of your code.
Types of Scope:
- Global Scope: Variables accessible everywhere
- Function Scope: Variables accessible only within function
- 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 → globalBlock 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); // ReferenceErrorA 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:
- Environment Record: Stores variables and function declarations
- 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 → nullThe 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.
┌───────────────────────────┐
│ Call Stack (Empty?) │
└───────────────────────────┘
│
↓
┌───────────────────────────┐
│ Check Microtask Queue │
│ (Promise callbacks, etc) │
└───────────────────────────┘
│
↓
┌───────────────────────────┐
│ Check Task Queue │
│ (setTimeout, events) │
└───────────────────────────┘
│
↓
┌───────────────────────────┐
│ Render (if needed) │
└───────────────────────────┘
│
└──────→ Repeat
Event Loop Algorithm:
- Execute all code in call stack
- When call stack is empty, check microtask queue
- Execute all microtasks
- If call stack still empty, take one task from task queue
- Execute that task
- Repeat from step 2
The task queue (also called macrotask queue or callback queue) holds:
setTimeoutcallbackssetIntervalcallbackssetImmediate(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' loggedThe microtask queue has higher priority than the task queue and holds:
- Promise callbacks (
.then,.catch,.finally) queueMicrotask()MutationObservercallbacksprocess.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' loggedPriority Order:
1. Synchronous code (call stack)
2. Microtasks (Promise callbacks)
3. Macrotasks (setTimeout, setInterval)
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/setIntervalfetch/XMLHttpRequest- DOM APIs (
addEventListener, etc.) localStorage/sessionStorageconsole(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)
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 2Let'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 2Step-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
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:
- JavaScript is single-threaded but can handle asynchronous operations
- Call stack manages function execution in LIFO order
- Memory heap stores objects and variables
- Execution contexts are created in two phases: creation and execution
- Hoisting moves declarations to the top of their scope
- Scope chain determines variable accessibility
- Event loop enables asynchronous behavior by managing queues
- Microtasks have priority over macrotasks
- Web APIs handle async operations outside the JS engine
- Understanding execution flow is crucial for debugging and optimization
- 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