Hello and welcome back.
In our previous lesson, we established the theoretical foundation of JavaScript's concurrency model. We identified the key components—the call stack, Web APIs, the event loop, and the crucial distinction between the macrotask and microtask queues. We learned the high-level algorithm: after the initial script runs, the event loop processes one macrotask, then completely drains the microtask queue, before considering rendering and the next macrotask.
Today, we move from theory to application. Our entire focus will be on mastering this lesson's learning outcome: to predict the order of execution for a mix of synchronous code, setTimeout, and resolved/rejected Promises. We will use the model from our last lesson as a mental framework to trace the execution flow of several code snippets, from simple to complex. This skill is not just an academic exercise; it's essential for debugging and writing predictable, robust asynchronous code.
The Rules of Engagement
Let's start with a quick recap of the execution order rules, which will be our guide throughout this lesson:
- Synchronous Code First: All synchronous code in the current execution context is run to completion. Anything that is not inside a callback or handler runs immediately.
- Queue Asynchronous Tasks: As the synchronous code runs, calls to asynchronous APIs like
setTimeoutand Promise handlers (.then(),.catch()) don't execute their callbacks immediately. Instead, they are placed in their respective queues.setTimeout,setInterval, I/O, UI events schedule macrotasks.- Promise handlers (
.then,.catch,.finally) schedule microtasks.
- Process Microtasks: Once the call stack is empty, the event loop processes all tasks in the microtask queue. If a microtask schedules another microtask, that new task is added to the end of the same queue and will also run before moving on.
- Process One Macrotask: After the microtask queue is completely empty, the event loop picks up one task from the macrotask queue and executes it.
- Repeat: The cycle (run all microtasks, then run one macrotask) repeats.
A Foundational Example
Let's begin by analyzing a classic code snippet that mixes all three types of operations: synchronous, a macrotask, and a microtask.
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
Before looking at the answer, try to predict the output using the rules above.
Now, let's read a concise explanation that breaks down this exact scenario.
Tasks, microtasks, queues and schedules
The article 'Tasks, microtasks, queues and schedules' by Jake Archibald provides a perfect step-by-step analysis of this code.
Read the sections 'Why this happens' and the animated step-by-step breakdown that follows it. Focus on how setTimeout schedules a task (macrotask) and how Promises schedule microtasks, leading to the predictable execution order.
As the article explains, the execution proceeds as follows:
- Synchronous Pass:
console.log('script start')runs. Output:script startsetTimeoutis called. Its callback is scheduled as a macrotask.Promise.resolve().then(...)is called. The first.then()callback is scheduled as a microtask.console.log('script end')runs. Output:script start,script end
- End of Initial Script: The call stack is now empty.
- Microtask Phase: The event loop processes the microtask queue.
- The first microtask runs:
console.log('promise1'). Output:script start,script end,promise1. - This
.then()completes, and since it returnsundefined(which is promise-like forundefined), it schedules the next.then()callback as a new microtask. - The event loop sees the microtask queue is not empty and runs the new microtask:
console.log('promise2'). Output:script start,script end,promise1,promise2.
- The first microtask runs:
- Macrotask Phase: The microtask queue is now empty. The event loop picks one task from the macrotask queue.
- The
setTimeoutcallback runs:console.log('setTimeout'). Final Output:script start,script end,promise1,promise2,setTimeout.
- The
Visualizing the Queues in Action
Watching this process unfold visually can solidify the mental model. We'll revisit the quiz from the end of our last lesson, but this time our goal is to actively trace it, not just test our knowledge.
JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
Lydia Hallie's 'JavaScript Visualized' video provides a superb animated walkthrough of a similar, slightly more complex scenario. Watching the tasks move between the queues is incredibly helpful.
Watch the quiz section from 08:34 to 11:03. As the code is presented, pause and trace the execution yourself on a piece of paper. Then, watch her animated explanation to see how each step maps to the call stack, microtask queue, and task (macrotask) queue.
The key takeaway from that video's example is seeing how a microtask (queueMicrotask) can itself add another microtask to the queue, and how the event loop diligently empties that entire queue before ever considering the setTimeout callback waiting in the macrotask queue.

A More Complex Challenge
You're now ready for a more intricate puzzle. This one includes a macrotask being scheduled from within a microtask, a common pattern in real-world code.
Carefully analyze the following code snippet. Write down what you believe the final output will be.
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4)));
Promise.resolve().then(() => console.log(5));
setTimeout(() => console.log(6));
console.log(7);
Once you have your prediction, let's consult a resource that provides a detailed, step-by-step breakdown of the solution.
Event loop: microtasks and macrotasks
The article 'Event loop: microtasks and macrotasks' on javascript.info contains this exact problem in its 'Tasks' section. It provides an excellent, clear explanation of the execution flow.
Read the section at the end of the article titled 'Tasks', specifically the question 'What will be the output of this code?'. Compare their step-by-step analysis with your own.
Let's recap the logic here, as it's a perfect illustration of all the rules working together:
- Synchronous Pass:
console.log(1)andconsole.log(7)execute.- Output so far:
1,7
- Queueing Pass:
- Two
setTimeoutcalls add() => console.log(2)and() => console.log(6)to the macrotask queue. - Three
.thencallbacks are added to the microtask queue:() => console.log(3),() => setTimeout(...), and() => console.log(5).
- Two
- End of Script.
- Microtask Phase (drain the entire queue):
- First microtask runs:
console.log(3). - Second microtask runs:
setTimeout(() => console.log(4)). This schedules a new macrotask. The macrotask queue now contains callbacks for2,6, and4. - Third microtask runs:
console.log(5). - Output so far:
1,7,3,5
- First microtask runs:
- Macrotask Phase (one at a time):
- The microtask queue is empty. The event loop takes the first item from the macrotask queue:
() => console.log(2). - Output so far:
1,7,3,5,2 - The event loop checks for microtasks again (there are none). It takes the next macrotask:
() => console.log(6). - Output so far:
1,7,3,5,2,6 - The event loop checks for microtasks again (none). It takes the final macrotask:
() => console.log(4). - Final Output:
1,7,3,5,2,6,4
- The microtask queue is empty. The event loop takes the first item from the macrotask queue:
Conclusion
In this lesson, we put the theory of the JavaScript concurrency model into practice. By systematically applying the rules of execution, we can reliably predict the output of even complex asynchronous code.
Key Takeaways:
- Prediction is a Process: Tracing execution is not about guessing; it's a step-by-step process of identifying synchronous code, queuing microtasks and macrotasks, and then simulating the event loop's behavior.
- Microtasks Have Priority: The most critical rule is that the event loop will always completely empty the microtask queue after the current script or a macrotask finishes, before doing anything else.
- Tasks Can Schedule Other Tasks: As we saw, a microtask (from a Promise) can schedule a new macrotask (
setTimeout), and a macrotask could schedule a new microtask. Understanding how new tasks are added to the queues is part of the puzzle.
What's Next?
You now have a solid grasp of when asynchronous code runs. In the next lesson, we will shift our focus to the core mechanism of modern asynchronous JavaScript: Promises. We will explore the three states of a Promise (pending, fulfilled, rejected), how they transition between these states, and why they are a significant improvement over traditional callbacks. This will build directly on your understanding of the microtask queue.