Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 37

Understanding JavaScript's Concurrency Model

Recommended resources

Hello! Welcome back.

In our last module, we focused on JavaScript's synchronous object-oriented features, culminating in how ES6 classes are a "syntactic sugar" over the underlying prototypal inheritance model. Now, we're making a significant but exciting shift to one of the most critical and defining aspects of the language: its asynchronous behavior.

Today's lesson addresses the first learning outcome of our new module on Asynchronous JavaScript. Our goal is to explain the components of the JavaScript concurrency model: the call stack, event loop, and task queues (macro and micro). You'll learn how JavaScript, despite being single-threaded, can handle tasks like network requests, timers, and user interactions without freezing. Understanding this model is fundamental to writing performant, non-blocking code, a core skill for both frontend and backend development.

The Conundrum: A Single Thread

At its core, the JavaScript runtime is single-threaded. This means it has only one call stack and can execute only one piece of code at a time. If a long-running task occupies that thread, the entire program—including the user interface in a browser—becomes unresponsive. This is known as "blocking."

How does a language with this limitation manage to be so effective for I/O-heavy applications? The answer lies not just in the JavaScript engine itself (like V8), but in the larger environment it runs in (like a browser or Node.js), which provides additional capabilities.

This classic talk from JSConf EU by Philip Roberts provides the best conceptual introduction to this topic. It masterfully visualizes the problem of blocking and introduces the main components that solve it.

What the heck is the event loop anyway? | Philip Roberts | JSConf EU

To start, let's watch a segment from 'What the heck is the event loop anyway?'. Philip Roberts will introduce the main components and demonstrate how the call stack works and why synchronous, slow operations are a problem.

Watch from 01:30 to 11:07. As you watch, focus on: The Call Stack: How it operates on a Last-In, First-Out (LIFO) basis to track function execution. Blocking: The visual demonstration of the browser UI freezing when a synchronous, slow task is on the call stack. The Solution: The introduction of asynchronous callbacks as the mechanism to prevent blocking.


The Concurrency Model: A Team of Specialists

As you saw in the video, the JavaScript engine itself is only part of the story. The full concurrency model involves several distinct components working together.

JavaScript Event Loop Concurrency Model Diagram
This diagram shows the main components of the JavaScript concurrency model. The JS engine handles the Call Stack, while the browser environment provides Web APIs and the queues. The Event Loop acts as the coordinator between them.

Let's break down the roles of these key players:

  1. Call Stack: Tracks the execution of synchronous code. As we've seen, it's a LIFO data structure. When you call a function, it's pushed onto the stack. When it returns, it's popped off.
  2. Web APIs: These are APIs provided by the browser environment, not the JavaScript engine. They handle asynchronous tasks in the background. Examples include the DOM APIs, setTimeout, setInterval, and fetch. When you call setTimeout, you're not telling JavaScript to pause; you're asking a Web API to start a timer.
  3. Task Queue (or Macrotask Queue): This is a First-In, First-Out (FIFO) queue. When a Web API finishes its task (e.g., a timer expires, or data from a network request arrives), it places the associated callback function into the Task Queue.
  4. Event Loop: This is the orchestrator. Its job is simple but crucial: it continuously checks if the Call Stack is empty. If it is, the Event Loop takes the first task from the Task Queue and pushes it onto the Call Stack to be executed.

The next segment of the same video perfectly illustrates how these pieces work together to handle an asynchronous setTimeout call without blocking.

What the heck is the event loop anyway? | Philip Roberts | JSConf EU

Let's continue with Philip Roberts' talk to see the full model in action.

Watch from 11:07 to 14:55. Pay close attention to the journey of a setTimeout callback: from the call stack to the Web API, then to the Task Queue, and finally back onto the call stack via the Event Loop.

This simple, elegant model is how JavaScript achieves its "non-blocking" behavior. The synchronous part of your code runs to completion, and the asynchronous callbacks wait patiently in the queue to be executed once the main thread is free.


A Tale of Two Queues: Macrotasks and Microtasks

The model we've discussed so far is accurate, but there's a critical layer of detail missing. The "Task Queue" is more accurately called the Macrotask Queue. There is a second, separate queue with a higher priority: the Microtask Queue.

Understanding the distinction between these two queues is essential for correctly predicting the execution order of complex asynchronous code, especially when Promises are involved.

  • Macrotasks: These include callbacks from setTimeout, setInterval, I/O operations, and UI rendering events. Only one macrotask is processed per "tick" of the event loop.
  • Microtasks: These primarily include callbacks from Promise handlers (.then(), .catch(), .finally()) and queueMicrotask(). The event loop will execute all available microtasks after a macrotask completes, before moving on to the next macrotask or rendering.

This video from Lydia Hallie offers a modern and clear visualization that introduces the Microtask Queue and explains its higher priority.

JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue

The video 'JavaScript Visualized - Event Loop' provides an excellent explanation of the Microtask Queue and its relationship to Promises.

Watch from 02:47 to 08:22. The first part recaps macrotasks (which she calls the 'task queue') with setTimeout. The crucial part begins at 06:08, where she introduces the Microtask Queue and demonstrates its higher priority using a fetch (Promise) example.

To solidify this concept, let's turn to a textual resource that lays out the rules clearly.

Event loop: microtasks and macrotasks

The article 'Event loop: microtasks and macrotasks' from javascript.info provides a concise and accurate definition of the two queues and a clear diagram of the event loop's full cycle.

Read the section titled 'Macrotasks and Microtasks'. Pay special attention to the example code mixing setTimeout and Promise.resolve, and the diagram illustrating the event loop's processing order.

JavaScript Concurrency Model: Call Stack, Event Loop, Microtask and Macrotask Queues
This diagram explicitly lists examples of functions that create microtasks (like Promise callbacks and `queueMicrotask`) and macrotasks (like `setTimeout`). The Event Loop always prioritizes draining the Microtask Queue before processing a single task from the Macrotask Queue.

The Event Loop Algorithm, Refined

With our new knowledge, we can define a more precise algorithm for the event loop:

  1. Execute the initial synchronous script until the call stack is empty.
  2. Start the loop:
    a. Dequeue and execute one macrotask from the macrotask queue (if one exists).
    b. After the macrotask completes, execute all tasks in the microtask queue until it's empty. If any of these microtasks queue new microtasks, they are also executed in this same step.
    c. If needed, the browser may perform rendering updates.
    d. If the macrotask queue is empty, wait for a new macrotask to appear.
    e. Go back to step 2a.

Putting It All Together: A Test of Understanding

The best way to confirm you've understood this model is to predict the execution order of a script that mixes synchronous code, macrotasks, and microtasks. The video from Lydia Hallie concludes with a great quiz.

JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue

Let's test your understanding by working through a practical example.

Watch from 08:42 to 10:51. Pause the video when the code appears (at 08:58) and try to predict the output yourself based on the rules we've just covered. Then, watch the walkthrough to see if you were right.

Here is a step-by-step breakdown of that quiz, reinforcing the logic:

  1. Synchronous Code Runs First: console.log(5) is executed. Output: 5.
  2. Queueing Tasks:
    • Promise.resolve().then(...): The .then callback (() => console.log(1)) is added to the microtask queue.
    • setTimeout(...): The callback (() => console.log(2)) is passed to the Web API. After its timer (0ms, but still asynchronous) completes, the callback is added to the macrotask queue.
    • queueMicrotask(...): The first callback (() => { console.log(3); ... }) is added to the microtask queue.
  3. End of Script: The main script finishes, and the call stack is empty. The event loop takes over.
  4. Microtask Phase: The event loop checks the microtask queue.
    • The first microtask (() => console.log(1)) is executed. Output: 5, 1.
    • The second microtask (() => { console.log(3); queueMicrotask(() => console.log(4)); }) is executed.
      • It logs 3. Output: 5, 1, 3.
      • It queues a new microtask: () => console.log(4).
    • The event loop must empty the microtask queue before continuing. It finds the new microtask and executes it. Output: 5, 1, 3, 4.
  5. Microtask Queue is Empty: The microtask phase is now complete.
  6. Macrotask Phase: The event loop checks the macrotask queue.
    • It finds the setTimeout callback (() => console.log(2)) and executes it. Output: 5, 1, 3, 4, 2.
  7. Final State: All queues are empty. The event loop waits for new tasks.

Conclusion

In this lesson, we deconstructed the JavaScript concurrency model to understand how a single-threaded language can perform non-blocking asynchronous operations.

Key Takeaways:

  • The Model's Components: The system relies on the Call Stack for synchronous execution, Web APIs to handle async tasks in the background, and the Event Loop to orchestrate moving callbacks onto the stack from two distinct queues.
  • Macrotasks vs. Microtasks: Callbacks from timers (setTimeout) and I/O go into the Macrotask Queue. Promise handlers (.then) go into the higher-priority Microtask Queue.
  • Execution Order: The Event Loop processes one macrotask, then completely drains the microtask queue, then performs rendering, and repeats. This prioritization is key to predicting execution flow.

What's Next?

You now have the foundational theory of the concurrency model. In our next lesson, we will put this knowledge into practice. We'll focus on the second learning outcome: "Predict the order of execution for a mix of synchronous code, setTimeout, and resolved/rejected Promises." We'll work through more complex examples to solidify your ability to trace the flow of execution through the different queues.