Skip to main content

Javascript Event Loop Call Stack And Task Queue Explained

· 4 min read
Sivabharathy

JavaScript is often described as single-threaded, non-blocking, asynchronous, and concurrent. While these terms can seem daunting, the underlying concepts are straightforward when you understand how the Event Loop, Call Stack, Microtask Queue, and Macrotask Queue work together. In this article, we’ll demystify these concepts with explanations and code examples.


The Call Stack

The Call Stack is where JavaScript keeps track of function execution. It operates on the "last in, first out" (LIFO) principle. When a function is called, it’s added to the top of the stack. Once the function finishes execution, it’s removed from the stack.

Example:

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

function sayGoodbye() {
console.log("Goodbye!");
}

function main() {
greet();
sayGoodbye();
}

main();

Execution Flow:

  1. main() is added to the stack.
  2. greet() is called and added to the stack.
  3. console.log('Hello!') runs and is removed from the stack.
  4. greet() finishes and is removed.
  5. sayGoodbye() is added to the stack.
  6. console.log('Goodbye!') runs and is removed.
  7. sayGoodbye() finishes and is removed.
  8. main() finishes and is removed.

The Event Loop

The Event Loop ensures that JavaScript operations run smoothly by coordinating tasks between the Call Stack and Task Queues. It monitors the call stack and queues, pushing tasks from the queues into the stack when the stack is empty.


Microtask Queue

The Microtask Queue has the highest priority. It handles tasks like resolved promises and mutation observers. After the current operation in the call stack is complete, the event loop processes all microtasks before moving on to macrotasks.

Example with Promises:

console.log("Script Start");

setTimeout(() => console.log("Macrotask: setTimeout"), 0);

Promise.resolve().then(() => console.log("Microtask: Promise"));

console.log("Script End");

Output:

Script Start
Script End
Microtask: Promise
Macrotask: setTimeout

Explanation:

  1. The synchronous code (console.log) runs first.
  2. The setTimeout callback is queued in the macrotask queue.
  3. The Promise callback is queued in the microtask queue.
  4. Microtasks are executed first, so Microtask: Promise logs before the setTimeout callback.

Macrotask Queue

The Macrotask Queue handles tasks like setTimeout, setInterval, and I/O events. These tasks are processed after the microtask queue is empty.

Example with Timers:

console.log("Script Start");

setTimeout(() => console.log("Macrotask 1: setTimeout"), 0);
setTimeout(() => console.log("Macrotask 2: setTimeout"), 0);

console.log("Script End");

Output:

Script Start
Script End
Macrotask 1: setTimeout
Macrotask 2: setTimeout

Explanation:

  1. Synchronous code runs first.
  2. Both setTimeout callbacks are added to the macrotask queue.
  3. The event loop processes them in FIFO order after the call stack is empty.

How They Work Together

Here’s the sequence:

  1. Call Stack Execution: Functions execute one at a time, in order.
  2. Microtask Processing: Once the stack is empty, the event loop processes all tasks in the microtask queue.
  3. Macrotask Processing: When the microtask queue is empty, the event loop processes tasks from the macrotask queue.

Combined Example:

console.log("Script Start");

setTimeout(() => console.log("Macrotask: setTimeout"), 0);

Promise.resolve()
.then(() => console.log("Microtask 1: Promise"))
.then(() => console.log("Microtask 2: Promise"));

console.log("Script End");

Output:

Script Start
Script End
Microtask 1: Promise
Microtask 2: Promise
Macrotask: setTimeout

Explanation:

  1. The synchronous console.log calls execute first.
  2. The setTimeout callback goes to the macrotask queue.
  3. The first Promise resolution goes to the microtask queue, followed by the second .then().
  4. Microtasks are executed before the macrotask, ensuring Microtask 1 and Microtask 2 finish first.

Visualizing the Event Loop

  • Call Stack: Functions are added and removed like stacking dishes.
  • Microtask Queue: High-priority tasks (e.g., promises) are queued and executed first.
  • Macrotask Queue: Lower-priority tasks (e.g., timers) are executed after microtasks.

Key Takeaways

  1. Event Loop ensures JavaScript is non-blocking and responsive.
  2. Microtasks (e.g., promises) always execute before macrotasks (e.g., timers).
  3. Understanding task prioritization helps avoid unexpected behavior in asynchronous code.

Conclusion

By understanding the Call Stack, Event Loop, Microtask Queue, and Macrotask Queue, you can write efficient and responsive JavaScript code. Use these concepts to debug, optimize, and handle complex asynchronous operations effectively.

Happy coding! 🎉