Skip to main content

Welcome to a Grasp lesson

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

Lesson 17

Asynchronous JavaScript: Call Stack, Event Loop, and Callback Queue

Recommended resources

Hello! Welcome to the first lesson in our module on Asynchronous JavaScript. In the previous module, we focused on mastering the synchronous parts of modern JavaScript, like ES6 syntax and classes. Now, we're shifting gears to understand how JavaScript handles tasks that don't complete instantly, such as fetching data from a server—a core requirement for connecting your React front-end to a Laravel back-end.

Today's learning outcome is to explain the roles of the call stack, event loop, and callback queue in handling async operations. This is the theoretical foundation for everything else we'll do with asynchronous code. Understanding this model is what separates developers who just use async/await from those who truly understand why it works and can debug complex asynchronous bugs.

Why JavaScript Needs an Asynchronous Model

You've probably heard that JavaScript is a single-threaded language. This means it has only one call stack and can only do one thing at a time.

Imagine a chef who can only perform one action at a time. If they start a time-consuming task, like slow-roasting a turkey for 3 hours, they can't do anything else—no chopping vegetables, no washing dishes. The entire kitchen comes to a halt. This is called blocking. In a browser, if the JavaScript thread is blocked by a slow task, the entire user interface freezes. The user can't click buttons, can't scroll, and the page becomes unresponsive.

To see this in action and understand the role of the call stack in synchronous code, let's watch a segment from one of the most famous talks in the JavaScript community.

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

In 'What the heck is the event loop anyway?', Philip Roberts gives a brilliant and accessible explanation of the JavaScript runtime. This first part will introduce the call stack and demonstrate the problem of 'blocking'.

Watch from 04:13 to 10:22. Focus on how function calls are pushed onto and popped off the call stack. Pay close attention to the browser demo at the end (09:10) which shows what happens when the stack is blocked by slow code.

As you saw, if we run slow, synchronous tasks directly on the call stack, the browser is stuck. So, how does JavaScript handle long-running operations like API requests (fetch) or timers (setTimeout) without freezing? It uses an asynchronous, non-blocking model.


The Asynchronous Components: Beyond the Engine

The key insight is that the JavaScript engine (like Chrome's V8) doesn't work in isolation. The environment it runs in (like a browser or Node.js) provides additional tools to handle asynchronicity.

Here are the main players:

  1. Call Stack: We've met this one. It's where synchronous code is executed, one function at a time.
  2. Web APIs: These are features provided by the browser, not the JS engine. They include the DOM, fetch, setTimeout, and event listeners. When you call a function like setTimeout, the JavaScript engine hands it off to the Web API to manage. The JS engine is then free to continue running other code.
  3. Callback Queue (or "Task Queue"): When a Web API finishes its job (e.g., a timer finishes counting down or data arrives from an API), it doesn't just shove the result back into the main program. Instead, it places the callback function associated with that task into the Callback Queue.
  4. Event Loop: This is the conductor of the whole orchestra. It has one simple job: to continuously monitor the Call Stack and the Callback Queue. If, and only if, the Call Stack is empty, the Event Loop takes the first item from the Callback Queue and pushes it onto the Call Stack to be executed.

Let's see this whole process visualized.

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

Now, let's continue with Philip Roberts' talk to see how these components work together to handle an asynchronous setTimeout call without blocking.

Watch from 10:22 to 14:55. This is the most crucial part of the lesson. Observe how setTimeout is passed to the Web APIs, allowing the stack to clear, and how the callback is moved to the queue and then back to the stack by the event loop.

This model explains why even setTimeout(callback, 0) doesn't run immediately. The callback is still passed to a Web API, then to the queue, and must wait for the stack to be completely empty before it can run.


A More Complete Picture: Microtasks and Macrotasks

The "Callback Queue" we've discussed is more accurately called the Macrotask Queue. It handles callbacks from setTimeout, setInterval, and UI events like clicks.

However, there's another, higher-priority queue called the Microtask Queue. This queue is primarily for the callbacks from Promises (which we'll cover in the next lesson).

Here’s the rule the Event Loop follows:

  1. Execute the current task on the call stack.
  2. After the task is done, check the Microtask Queue. Run all microtasks in the queue until it is completely empty.
  3. Check the Macrotask Queue. Run one macrotask.
  4. Repeat from step 2.

This means that Promise callbacks will always execute before setTimeout callbacks, even if both are ready at the same time. This distinction is vital for understanding modern asynchronous patterns.

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

The video 'JavaScript Visualized' by Lydia Hallie provides a clear, modern animation of this process, with a specific focus on the difference between the task queue (macrotasks) and the microtask queue.

Watch from 06:12 to 08:57. This section explains how Promise-based Web APIs like fetch use the microtask queue and demonstrates its priority over the macrotask queue.

To help all these concepts sink in, let's read through an article that explains this using a helpful analogy of a chef in a kitchen.

Async in JavaScript, Served Hot: The Chef's guide to the Event Loop

The article 'Async in JavaScript, Served Hot' uses a great analogy to explain the event loop. Reading this will reinforce what you've seen in the videos.

Read from the beginning down to the section 'A Peek behind the kitchen door: Microtasks vs Macrotasks'. Focus on how the chef (JS engine), kitchen helpers (Web APIs), and trays (queues) interact. The code example with the predicted output is a great summary.


Visualizing the Full Model

Here is a diagram that puts all the pieces together. It shows how synchronous code runs on the call stack, how async tasks are offloaded to Web APIs, and how their callbacks are placed in either the Microtask or Macrotask queue, ready for the Event Loop to process.

JavaScript Event Loop and Asynchronous Execution Model
This diagram shows the complete JavaScript runtime model. The Event Loop prioritizes the Microtask Queue over the Macrotask Queue, but only processes items from either queue when the Call Stack is empty.

Test Your Understanding

Based on everything we've covered, try to predict the order in which the numbers will be logged to the console in the following code snippet.

console.log('1');

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

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

Think it through, then check the solution below.

Click for the step-by-step explanation

The correct order is 1, 4, 3, 2. Here's why:

  1. console.log('1') is a synchronous task. It's pushed to the call stack, executed immediately, and logs 1.
  2. setTimeout(...) is an async task. Its callback is handed to the Web API. The timer is set for 0ms. setTimeout itself then pops off the stack.
  3. Promise.resolve().then(...) is an async task. The .then() callback is a microtask and is immediately placed in the Microtask Queue.
  4. console.log('4') is a synchronous task. It's pushed to the stack, executed, and logs 4.
  5. The initial script has now finished, and the call stack is empty. The Event Loop checks for work.
  6. The Event Loop checks the Microtask Queue first. It finds the callback from the Promise. It pushes this callback to the stack, which runs console.log('3'), logging 3.
  7. The Microtask Queue is now empty. The Event Loop now checks the Macrotask Queue. By this time, the 0ms timer has completed, and the Web API has moved the setTimeout callback into the Macrotask Queue.
  8. The Event Loop takes the setTimeout callback from the Macrotask Queue, pushes it to the stack, which runs console.log('2'), logging 2.

Conclusion

You've just learned the fundamental concurrency model of JavaScript. It might seem complex at first, but it all boils down to a few key ideas.

Key Takeaways:

  • JavaScript is single-threaded and has one Call Stack, which executes code one task at a time.
  • Slow synchronous code on the stack blocks the entire application.
  • Asynchronous operations (like setTimeout or fetch) are handed off to Web APIs to be handled in the background.
  • When an async operation is complete, its callback is placed in a queue:
    • Macrotask Queue for timers, UI events, etc.
    • Microtask Queue for Promise callbacks (.then, .catch).
  • The Event Loop constantly checks if the Call Stack is empty. If it is, it first processes all tasks in the Microtask Queue, and only then processes one task from the Macrotask Queue.

Preview of Next Lesson:
Now that you have the essential theory of how async operations work under the hood, we can start using the tools to manage them effectively. In the next lesson, we will dive into Promises. You will learn how to create and consume Promises using .then(), .catch(), and .finally() to handle asynchronous sequences of events in a clean and predictable way.