How JavaScript Actually Works: The Event Loop

In JavaScript, the event loop is the mechanism that processes and responds to events. It's responsible for executing code, collecting and processing events, and executing queued sub-tasks.

To grasp this concept fully, we need to delve into several interconnected components: the memory heap, call stack, web APIs, event loop, callback queue, and job queue.

The Memory Heap

The memory heap is where JavaScript objects and data structures are stored. Unlike the stack, which is structured, the heap is less organized. When you create an object or an array, it's allocated in the heap.

The Call Stack

The call stack is a LIFO (Last In, First Out) data structure that keeps track of the currently executing function and its callers. When a function is invoked, it's pushed onto the stack. When it returns, it's popped off. This ensures proper function execution and return values.

Web APIs

Web APIs are provided by the browser or Node.js environment to handle asynchronous tasks. They include:

  • DOM manipulation

  • Timers (setTimeout, setInterval)

  • Fetch API

  • File system operations

When you call a Web API, it doesn't block the main thread. Instead, it schedules a callback function to be executed later.

The Event Loop

The event loop is the heart of JavaScript's asynchronous behavior. It constantly checks:

  1. Call Stack: If empty, it moves to the next step.

  2. Callback Queue: If not empty, it takes the first callback, pushes it onto the call stack, and starts executing it.

  3. Job Queue (Microtask Queue): If not empty, it processes microtasks before returning to the callback queue.

The Callback Queue

This is where callback functions from asynchronous operations are placed. When a Web API finishes its task, it pushes the corresponding callback into the callback queue.

The Job Queue (Microtask Queue)

This queue holds high-priority tasks like Promise resolutions and MutationObserver callbacks. It's processed before the callback queue in each event loop iteration.

How It All Works Together

  1. Code Execution: Your JavaScript code starts running, and functions are pushed onto the call stack.

  2. Asynchronous Operations: When you encounter an asynchronous operation, the corresponding callback is placed in the callback or job queue.

  3. Call Stack Empties: Once the current function finishes, the call stack is empty.

  4. Event Loop: Checks the job queue first, then the callback queue.

  5. Callback or Microtask Execution: The selected callback or microtask is pushed onto the call stack and executed.

Examples 1 :

console.log('start');

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

console.log('end');

/*
Output: 
start
end
timer
*/

Explanation:

  • console.log('start') and console.log('end') are pushed onto the call stack and executed immediately.

  • setTimeout is a Web API, so its callback is placed in the callback queue.

  • After the main script finishes, the event loop checks the callback queue and pushes the setTimeout callback onto the call stack.

  • console.log('timer') is executed.

Examples 2 :

console.log('message 1 : sync');
setTimeout(function() {
   console.log('message 2 : setTimeOut');
}, 0);
var promise = new Promise(function(resolve, reject) {
   resolve();
});
promise.then(function(resolve) {
   console.log('message 3 : promise');
})
.then(function(resolve) {
   console.log('message 4 : promise');
});
console.log('message 5 : sync');

/*
Output:
message 1 : sync
message 5 : sync
message 3 : promise
message 4 : promise
message 2 : setTimeOut
*/

Explanation:

  1. console.log('message 1 : sync'); is executed synchronously, printing "message 1 : sync".

  2. setTimeout(function() { console.log('message 2 : setTimeOut'); }, 0); schedules a callback function to log "message 2 : setTimeOut" after 0 milliseconds. This callback is placed in the task queue.

  3. var promise = new Promise(function(resolve, reject) { resolve(); }); creates a Promise that is immediately resolved.

  4. promise.then(function(resolve) { console.log('message 3 : promise'); }) attaches a callback to the Promise. This callback is placed in the job queue (microtask queue).

  5. .then(function(resolve) { console.log('message 4 : promise'); }); attaches another callback to the Promise, also placed in the job queue.

  6. console.log('message 5 : sync'); is executed synchronously, printing "message 5 : sync".

Examples 3 :

console.log('message 1 : sync');
setTimeout(function() {
console.log('message 2 : setTimeOut');
}, 0);
var promise = new Promise(function(resolve, reject) {
resolve();
});
promise.then(function(resolve) {
setTimeout(function() {
console.log('message 3 : setTimeOut - promise');
}, 5)
})
.then(function(resolve) {
console.log('message 4 : promise');
});
console.log('message 5 : sync');


/*
Output:
message 1 : sync
message 5 : sync
message 4 : promise
message 3 : setTimeOut - promise
message 2 : setTimeOut
*/

Explanation:

  1. console.log('message 1 : sync'); is executed synchronously, printing "message 1 : sync".

  2. setTimeout(function() { console.log('message 2 : setTimeOut'); }, 0); schedules a callback function to log "message 2 : setTimeOut" after 0 milliseconds. This callback is placed in the task queue.

  3. var promise = new Promise(function(resolve, reject) { resolve(); }); creates a Promise that is immediately resolved.

  4. promise.then(function(resolve) { setTimeout(function() { console.log('message 3 : setTimeOut - promise'); }, 5) }) attaches a callback to the Promise. This callback is placed in the job queue (microtask queue). Inside this callback, another setTimeout is scheduled with a delay of 5ms. This inner setTimeout callback will be placed in the task queue.

  5. .then(function(resolve) { console.log('message 4 : promise'); }); attaches another callback to the Promise, also placed in the job queue.

  6. console.log('message 5 : sync'); is executed synchronously, printing "message 5 : sync".

Event Loop and Job Queue:

  • The event loop processes the call stack first.

  • Once the call stack is empty, it checks the job queue (microtask queue).

  • The first Promise's then callback is executed, logging "message 4 : promise".

  • The second Promise's then callback is executed, logging "message 3 : setTimeOut - promise" after a 5ms delay due to the inner setTimeout.

  • After the job queue is empty, the event loop checks the task queue and finds the setTimeout callback.

  • Finally, "message 2 : setTimeOut" is printed.

Conclusion

Understanding the event loop is crucial for writing efficient and asynchronous JavaScript code. By grasping the interplay between the memory heap, call stack, web APIs, event loop, callback queue, and job queue, you can better predict how your code will execute and troubleshoot potential issues.

1
Subscribe to my newsletter

Read articles from Aparna Udayakumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aparna Udayakumar
Aparna Udayakumar