Module: Asynchronous JS

Event Loop

JavaScript Essentials: Asynchronous JS & The Event Loop

JavaScript is single-threaded, meaning it can only execute one task at a time. However, it handles many operations concurrently thanks to its asynchronous nature and the Event Loop. This document explains how this works.

1. Synchronous vs. Asynchronous JavaScript

  • Synchronous: Code executes line by line, in order. Each operation must complete before the next one starts. This can block the main thread, making the UI unresponsive if a long-running task is executed.

    console.log("Start");
    let result = expensiveOperation(); // Blocks until complete
    console.log(result);
    console.log("End");
    
  • Asynchronous: Allows the program to initiate a potentially long-running operation and continue executing other code without waiting for that operation to complete. When the operation does complete, a callback function is executed. This prevents blocking the main thread.

    console.log("Start");
    setTimeout(() => {
      console.log("Inside setTimeout");
    }, 2000); // 2 seconds
    console.log("End");
    

    Output:

    Start
    End
    (after 2 seconds) Inside setTimeout
    

    Notice how "End" is logged before "Inside setTimeout", even though the setTimeout call appears earlier in the code. This is because setTimeout is asynchronous.

2. Key Components

  • Call Stack: A LIFO (Last-In, First-Out) data structure that keeps track of the functions currently being executed. When a function is called, it's pushed onto the stack. When it completes, it's popped off. JavaScript can only execute code that's currently on the call stack.

  • Web APIs (Browser APIs/Node APIs): Features provided by the browser (or Node.js) that are not part of the core JavaScript language. Examples include setTimeout, setInterval, fetch, DOM manipulation, and event listeners. These APIs allow JavaScript to perform tasks outside of the single thread.

  • Task Queue (Callback Queue): A queue that holds callback functions that are ready to be executed. These callbacks are placed in the queue by Web APIs when their asynchronous operations complete.

  • Event Loop: The heart of asynchronous JavaScript. It continuously monitors the Call Stack and the Task Queue. If the Call Stack is empty, it takes the first callback from the Task Queue and pushes it onto the Call Stack for execution.

3. How the Event Loop Works: A Step-by-Step Example

Let's break down the setTimeout example from earlier:

  1. console.log("Start");: console.log is pushed onto the Call Stack, executed, and popped off. "Start" is logged.

  2. setTimeout(() => { console.log("Inside setTimeout"); }, 2000);:

    • setTimeout is pushed onto the Call Stack, executed.
    • setTimeout is a Web API function. It starts a timer for 2000 milliseconds.
    • setTimeout immediately returns, and is popped off the Call Stack. Crucially, the timer runs in the background, managed by the browser (or Node.js).
    • The callback function () => { console.log("Inside setTimeout"); } is not executed yet.
  3. console.log("End");: console.log is pushed onto the Call Stack, executed, and popped off. "End" is logged.

  4. Timer Completes: After 2000 milliseconds, the Web API (the timer) places the callback function () => { console.log("Inside setTimeout"); } into the Task Queue.

  5. Event Loop's Role: The Event Loop continuously checks:

    • Is the Call Stack empty? (Yes, it is)
    • Is there anything in the Task Queue? (Yes, there's the setTimeout callback)
  6. Callback Execution: The Event Loop takes the setTimeout callback from the Task Queue and pushes it onto the Call Stack.

  7. console.log("Inside setTimeout");: The callback function is executed, logging "Inside setTimeout". The callback is then popped off the Call Stack.

4. Visual Representation

+-----------------+     +-----------------+     +-----------------+
|    Call Stack   |     |   Task Queue    |     |   Web APIs       |
+-----------------+     +-----------------+     +-----------------+
|                 |     |                 |     | setTimeout (timer)|
|                 |     |                 |     | fetch (network) |
|                 |     |                 |     | DOM Events      |
+-----------------+     +-----------------+     +-----------------+
     ^                                          |
     |                                          |
     |  Event Loop:  If Call Stack is empty,   |
     |  move first task from Task Queue to     |
     |  Call Stack.                             |
     +------------------------------------------+

5. Important Considerations

  • Microtask Queue: There's also a Microtask Queue (used by Promises, MutationObserver, etc.). Microtasks have higher priority than tasks in the Task Queue. The Event Loop processes all microtasks before moving on to the Task Queue. This means Promises resolve before setTimeout callbacks.

  • Blocking the Event Loop: Long-running synchronous operations will block the Event Loop, making the application unresponsive. Avoid these by using asynchronous alternatives whenever possible.

  • process.nextTick() (Node.js): A Node.js specific function that adds a callback to the Microtask Queue. It has even higher priority than Promises.

6. Example with Promises

console.log("Start");

Promise.resolve().then(() => {
  console.log("Inside Promise");
});

setTimeout(() => {
  console.log("Inside setTimeout");
}, 0);

console.log("End");

Output:

Start
End
Inside Promise
Inside setTimeout

Explanation:

  1. Promise.resolve().then(...) creates a Promise that immediately resolves. The then callback is added to the Microtask Queue.
  2. setTimeout(..., 0) adds a callback to the Task Queue.
  3. Because the Microtask Queue has higher priority, the Promise callback is executed before the setTimeout callback.

Resources

Understanding the Event Loop is crucial for writing efficient and responsive JavaScript applications. By leveraging asynchronous programming and avoiding blocking operations, you can create a smooth user experience.