JavaScript is a single-threaded programming language. This means that JavaScript can do only one thing at a single point in time.

The main thread is responsible for executing JavaScript code, performing rendering tasks, and handling user interactions. Since JavaScript is single-threaded, it can perform only one task at a time on the main thread.

The JavaScript engine executes a script from the top of the file and works its way down. It creates the execution contexts, and pushes, and pops functions onto and off the call stack in the execution phase.

If a function takes a long time to execute, you cannot interact with the web browser during the function's execution because the page hangs.

A function that takes a long time to complete is called a blocking function. Technically, a blocking function blocks all the interactions on the webpage, such as mouse clicks.

The following example uses a big loop to simulate a blocking function:

function task(message) {
    // emulate time consuming task
    let n = 10000000000;
    while (n > 0){
        n--;
    }
    console.log(message);
}

console.log('Start script...');
task('Doing a task');
console.log('Done!');

The script hangs for a few seconds (depending on how fast the computer is) and issues the following output:

Start script...
Doing a task
Done!

The JavaScript Event Loop is a mechanism that enables JavaScript to perform non-blocking I/O operations despite being single-threaded. It allows asynchronous operations, such as handling user inputs, network requests, and timers, to be executed without blocking the main execution thread.

The Event Loop stands as a crucial element within the JavaScript runtime environment, functioning in the following manner:

  • Continuously monitoring the status of the call stack to determine if it's empty.
  • Upon encountering an empty call stack, all pending Microtasks from the Microtask Queue are introduced into the call stack.
  • When both the call stack and Microtask Queue are devoid of tasks, the event loop proceeds to dequeue tasks from the Task Queue and execute them.
  • Ensuring the prevention of a "starved event loop" remains pivotal for maintaining seamless JavaScript execution.

The Event Loop is a continuous process that coordinates the execution of tasks in JavaScript. It is composed of several components that work together to facilitate asynchronous programming:

  • Call Stack
  • Web APIs
  • Macrotask Queue
  • Microtask Queue
Call Stack

The call stack is a fundamental concept in JavaScript and plays a key role in understanding the language's asynchronous behavior. It is a data structure known as a Last In, First Out (LIFO) stack, which is responsible for keeping track of the function calls and their execution order. Whenever a function is called, it gets added to the call stack, and once it finishes executing, it is removed from the stack (sort of).

JavaScript is single-threaded, meaning it has only one call stack that can handle one task at a time. This is why blocking operations, such as time-consuming calculations or network requests, can freeze the browser's UI until the task is completed.

Here's an example:

function fifth() { 
    console.log("fifth")
}

function fourth() { 
    console.log("fourth")
    fifth() 
}

function third() { 
    console.log("third")
    fourth() 
}

function second() { 
    console.log("second")
    third() 
}

function first() {
    console.log("first") 
    second() 
}

first();

The execution flow is as follows:

  • The main thread starts by creating the global execution context.
  • The main thread pushes the global execution context onto the call stack.
  • The main thread executes first function call, a function execution context for first (or frame) is added to the call stack.
    call stack
  • The first function's code is executed line-by-line.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "first" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread executes second function call, a function execution context for second (or frame) is added to the call stack.
    call stack
  • The second function's code is executed line-by-line.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "second" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread executes third function call, a function execution context for third (or frame) is added to the call stack.
    call stack
  • The third function's code is executed line-by-line.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "third" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread executes fourth function call, a function execution context for fourth (or frame) is added to the call stack.
    call stack
  • The fourth function's code is executed line-by-line.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "fourth" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • fifth (or frame) is added to the call stack.
    call stack
  • The fifth function's code is executed line-by-line.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "fifth" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes function execution context for fifth, fourth, third, second and first consecutively from the call stack.
  • The main thread removes the global execution context from the call stack.
Web APIs

Web APIs are provided by the browser environment to extend JavaScript's functionality beyond its single-threaded nature. These APIs allow developers to access features like the DOM, timers, and network requests, enabling JavaScript to perform tasks asynchronously without blocking the call stack.

When a function utilizing a Web API is called, the API handles the task in the background and returns a callback function. This callback is then placed into a queue, waiting to be executed once the call stack is empty.

Here are some common examples of Web APIs: DOM (Document Object Model) API, Fetch API, Geolocation API, Canvas API, Web Storage API, etc.

Here's an example:

console.log('Start');

setTimeout(() => {
    console.log('Timeout');
}, 1000);

console.log('End');

The execution flow is as follows:

  • The main thread starts by creating the global execution context.
  • The main thread pushes the global execution context onto the call stack.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "Start" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread executes setTimeout function call.
  • The main thread creates a new function execution context and places it on the call stack.
  • The main thread registers the callback function and the delay with the Web API.
  • The Web API (e.g., the browser's timer system) sets up a timer that counts down in the background within the Web API environment from the specified delay (1000 milliseconds in this case).
  • The main thread removes the function execution context from the call stack.
  • When the timer expires after the specified delay, the Web API moves the callback function to the callback queue (or task queue).
  • The main thread continues to execute any synchronous code that follows.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "End" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • Once all the synchronous code has executed, the main thread removes the global execution context from the call stack.
  • The call stack is now empty, waiting for asynchronous callbacks to be executed.
  • The event loop starts processing the asynchronous callbacks that have been registered.
  • The event loop pushes the first task from the callback queue onto the call stack and creates a new function execution context for the setTimeout callback function.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "Timeout" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes the function execution context for setTimeout callback function from the call stack.
Tasks and the Task Queue

Tasks are scheduled, synchronous blocks of code. While executing, they have exclusive access to the Call Stack and can also enqueue other tasks. Between Tasks, the browser can perform rendering updates. Tasks are stored in the Task Queue, waiting to be executed by their associated functions. These tasks come from:

  • Timers (via setTimeout or setInterval)
  • Event listeners (when an event is triggered)
  • Network operations (when a response is received)

The Task Queue is a FIFO (First In, First Out) data structure.

Here's an example:

setTimeout(function a() {
    console.log("task A")            
}, 1000);

setTimeout(function b() {
    console.log("task B")
}, 500);

setTimeout(function c() {
    console.log("task C")
}, 0);

function d() {
    console.log("task D")
}

d();

The execution flow is as follows:

  • The main thread starts by creating the global execution context.
  • The main thread pushes the global execution context onto the call stack.
  • The main thread executes setTimeout function call, a function execution context for setTimeout is added to the call stack.
  • The main thread registers the callback function and the delay with the Web API.
    call stack
  • The Web API (e.g., the browser's timer system) sets up a timer that count down in the background within the Web API environment from the specified delay.
  • When the timer expires after the specified delay, the Web API moves the setTimeout callback function to the callback queue (or task queue).
  • The main thread removes the function execution context for the setTimeout function from the call stack.
  • The main thread executes the second setTimeout function call, a function execution context for the second setTimeout is added to the call stack.
  • The main thread registers the callback function and the delay with the Web API.
    call stack
  • The Web API (e.g., the browser's timer system) sets up a timer that count down in the background within the Web API environment from the specified delay.
  • When the timer expires after the specified delay, the Web API moves the second setTimeout callback function to the callback queue (or task queue).
  • The main thread removes the function execution context for the second setTimeout function from the call stack.
  • The main thread executes the third setTimeout function call, a function execution context for the third setTimeout is added to the call stack.
  • The main thread registers the callback function and the delay with the Web API.
    call stack
  • The Web API (e.g., the browser's timer system) sets up a timer that count down in the background within the Web API environment from the specified delay.
  • When the timer expires after the specified delay, the Web API moves the third setTimeout callback function to the callback queue (or task queue).
  • The main thread removes the function execution context for the third setTimeout function from the call stack.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread continues to execute any synchronous code that follows.
  • The main thread executes d function call, a function execution context for d (or frame) is added to the call stack.
    call stack
  • The d function's code is executed line-by-line.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "task D" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes function execution context for d from the call stack.
  • Once all the synchronous code has executed, the main thread removes the global execution context from the call stack.
  • The call stack is now empty, waiting for asynchronous callbacks to be executed.
  • The event loop starts processing the asynchronous callbacks that have been registered.
  • After 0 milliseconds, the event loop the task c from Web API to the callback queue.
    call stack
  • The event loop pushes the task c from the callback queue onto the call stack and creates a new function execution context for the setTimeout callback function.
    call stack
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "task C" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes the function execution context for the setTimeout callback function from the call stack.
  • After 500 milliseconds, the event loop the task b from Web API to the callback queue.
    call stack
  • The event loop pushes the task b from the callback queue onto the call stack and creates a new function execution context for the setTimeout callback function.
    call stack
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "task B" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes the function execution context for the setTimeout callback function from the call stack.
  • After 1000 milliseconds, the event loop the task a from Web API to the callback queue.
    call stack
  • The event loop pushes the next task from the callback queue onto the call stack and creates a new function execution context for the setTimeout callback function.
    call stack
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "task A" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes the function execution context for the setTimeout callback function from the call stack.
Microtasks and the Microtask Queue

Microtasks are similar to Tasks in that they're scheduled, synchronous blocks of code with exclusive access to the Call Stack while executing. Additionally, they are stored in their own FIFO (First In, First Out) data structure, the Microtask Queue. Microtasks differ from Tasks, however, in that the Microtask Queue must be emptied out after a Task completes and before re-rendering.

Microtasks and the Microtask Queue are also referred to as Jobs and the Job Queue.

The microtask queue holds tasks that are prioritized over tasks in the task queue. Microtasks include promises and mutation observer callbacks. When the call stack is empty and before fetching tasks from the task queue, the event loop first processes all tasks in the microtask queue. This ensures that microtasks are executed as soon as possible.

Here's an example:

console.log("Start");

const promise = new Promise((resolve, reject) => {
    console.log("Promise executor function started");
    resolve("Promise resolved successfully");
});

promise.then(function c(result) {
    console.log(result);
});

setTimeout(function d() {
    console.log("task D");
}, 0);

function e() {
    console.log("task E");
}

e();

console.log("End");

The execution flow is as follows:

  • The main thread starts by creating the global execution context.
  • The main thread pushes the global execution context onto the call stack.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "Start" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread executes Promise constructor call, and its execution context is created and pushed onto the call stack.
  • The executor function within the Promise constructor initializes the promise.
  • The main thread executes executor function call, and its execution context is created and pushed onto the call stack.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "Promise executor function started" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread executes resolve function call, and its execution context is created and pushed onto the call stack.
  • Calling resolve transitions the promise from the pending state to the fulfilled (or resolved) state.
  • The value "Promise resolved successfully" is set as the value of the promise.
  • The main thread removes function execution context for resolve from the call stack.
  • The executor function finishes its synchronous execution, and its execution context is popped off the call stack.
  • Once the executor function completes its synchronous execution, the promise constructor itself has no more code to run.
  • The Promise constructor completes, and its execution context is popped off the call stack.
  • The main thread executes .then() method call, and its execution context is created and pushed onto the call stack.
  • The main thread moves the callback function c to the microtask queue (or job queue).
    call stack
  • The main thread removes the execution context for then() from the call stack.
  • The main thread executes setTimeout function calls.
  • The main thread creates new function execution contexts and places it on the call stack.
  • The main thread registers the callback function and the delay with the Web API.
    call stack
  • The Web API (e.g., the browser's timer system) sets up a timer that count down in the background within the Web API environment from the specified delay.
  • When the timer expires after the specified delay, the Web API moves the callback function to the callback queue (or task queue).
  • The main thread removes the function execution contexts for setTimeout from the call stack.
  • The main thread continues to execute any synchronous code that follows.
  • The main thread executes e function call, and its execution context is created and pushed onto the call stack.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "task E" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes the execution context for e from the call stack.
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "End" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • Once all the synchronous code has executed, the main thread removes the global execution context from the call stack.
  • The call stack is now empty, waiting for asynchronous callbacks to be executed.
  • The event loop starts processing the asynchronous callbacks that have been registered.
  • The event loop proceeds to handle the microtask queue before handling any macrotasks.
  • If there are microtasks in the microtask queue, the event loop moves the microtask c from the microtask queue to the call stack and creates a new function execution context for the resolve callback function.
    call stack
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "Promise resolved successfully" to the console.
  • The main thread removes the function execution context for console.log from the call stack.
  • The main thread removes the function execution context for the callback function c from the call stack.
  • After all microtasks have been processed, the event loop proceeds to handle macrotasks.
  • After 0 milliseconds, the event loop the task d from Web API to the callback queue.
    call stack
  • The event loop pushes the task d from the callback queue onto the call stack and creates a new function execution context for the setTimeout callback function.
    call stack
  • The main thread executes console.log function call, a function execution context for console.log (or frame) is added to the call stack.
  • The main thread logs "task D" to the console.
  • The main thread removes function execution context for console.log from the call stack.
  • The main thread removes the function execution context for the setTimeout callback function from the call stack.