· Tech · 5 min read
JavaScript Event Loop
Understanding how JavaScript handles asynchronous operations through the Event Loop, Macro Stack, and Micro Stack mechanisms.
When I started to tinker with JavaScript code to solve various problems, I learned to use setTimeout, setInterval, then Promise. I also learned that some libraries use requestAnimationFrame, and with some examples, I used it. At first, I was confused. Which callbacks are firing when, and why? Over time, I felt I “got” it and saw the pattern, except I did not. Then I had to learn this concept: The Event Loop.
This is a note of what I’ve learned about the Event Loop so far.
Two To Do Lists
The event loop is a mechanism that allows JavaScript to handle the complex requirements developers might have (btw, which are waaaay more complex today compared to, say, 20 years ago). Otherwise, it’s a single-threaded, simple scripting language that’s supposed to handle occasional onClick or onHover (do you remember DreamWeaver injecting onHover scripts that swapped images onHover?)
It is basically a stack, or two stacks actually, or to-do lists.
setTimeout callback, onClick callback, onLoad, Promise’s then—everything goes to these to-do lists, or stacks.
And there are two kind of stacks: the Macro Stack (sometimes written as Macro Stack) and the Micro Stack (sometimes written as Micro Stack).
Everything goes either one of them, and strictly speaking, JavaScript takes one item from the Macro Stack (sometimes written as Macro Stack) and executes it, then it picks up everything in the Micro Stack (sometimes written as Micro Stack). The first item in the Macro Stack is executing the code itself. So consider this:
setTimeout(() => {
console.log("1");
}, 0);
console.log("2");
setTimeout(() => {
console.log("3");
}, 0);
Promise.resolve().then(() => {
console.log("4");
});
console.log("5");
Promise.resolve().then(() => {
console.log("6");
});The code itself is parsed and executed (Macro Stack), and during the execution, the following happens:
- 1st setTimeout callback is queued in the Macro Stack (sometimes written as Macro Stack)
- output “2”
- 2nd setTimeout callback is queued in the Macro Stack (sometimes written as Macro Stack)
- 1st Promise then callback is queued in the Micro Stack (sometimes written as Micro Stack)
- output “5”
- 2nd Promise then callback is queued in the Micro Stack (sometimes written as Micro Stack); At this point, it moves to handling all of the Micro Stack (sometimes written as Micro Stack).
- output “4”
- output “6”; At this point, the Micro Stack (sometimes written as Micro Stack) is empty, it moves to the Macro Stack (sometimes written as Macro Stack).
- output “1”; At this point, it moves to the Micro Stack (sometimes written as Micro Stack), and it’s empty. So it moves to the Macro Stack (sometimes written as Macro Stack).
- output “3”; At this point, the Macro Stack (sometimes written as Macro Stack) is empty; All done;
Now, let’s replace Promise then with queueMicrotask, which queues a callback to the Micro Stack (sometimes written as Micro Stack), like so:
setTimeout(() => {
console.log("1");
}, 0);
console.log("2");
setTimeout(() => {
console.log("3");
}, 0);
queueMicrotask(() => {
console.log("4");
});
console.log("5");
queueMicrotask(() => {
console.log("6");
});Then modify the code a little, and see what’d happen:
setTimeout(() => {
console.log("1");
queueMicrotask(() => {
console.log("2");
queueMicrotask(() => {
console.log("3");
});
queueMicrotask(() => {
console.log("4");
});
});
}, 0);
console.log("5");
setTimeout(() => {
console.log("6");
}, 0);
queueMicrotask(() => {
console.log("7");
});
console.log("8");
queueMicrotask(() => {
console.log("9");
});What’d happen now, when it analyzes and executes the first callstack?:
- first setTimeout callback is queued in the Macro Stack
- output “5”
- second setTimeout callback is queued in the Macro Stack
firstqueueMicrotask callback queued in the Micro Stack- output “8”
secondqueueMicrotask callback queued in the Micro Stack; at this time, callstack is empty so it moves to the Micro Stack- executing the first Micro Stack callback, output “7”
- executing the second Micro Stack callback, output “9”; at this time, the Micro Stack is empty, so it moves to the Macro Stack
- executing the first Macro Stack callback, output “1”, then add one callback to the Micro Stack. Now it checks the Micro Stack before moving to the next item in the Macro Stack, and finds 1 newly added callback.
- executing the newly added Micro Stack callback, output “2” and add two more callbacks to the Micro Stack
- Again, it checks the Micro Stack before moving to the next item in the Macro Stack, and finds 2 newly added callbacks.
- executing the next Micro Stack callback, output “3”.
- executing the next Micro Stack callback, output “4”. At this time, the Micro Stack is empty so it moves to the Macro Stack
- output “6”
Just like that, you are able to queue more tasks inside a queued task. Now, if you queue a micro task in a callback inside the Micro Stack, it’d be called before it moves to the Macro Stack.
- first execute what is parsed.
- then execute all the callbacks in the Micro Stack, even the ones added during callback execution
- only when the Micro Stack is empty, it takes out just one callback from the Macro Stack, then moves to the Micro Stack; so if you add anything to the Micro Stack inside a Macro Stack callback, it will be called before the next callback in the Macro Stack
So the Micro Stack is really in a priority lane, or has this super “VIP pass” that lets you cut off all the callbacks queued in the Macro Stack.
I know this article is a bit obsessive and redundant, but this is how I understood how the event loop works for sure.
This is the basics of the event loop in JavaScript. However, there is one more thing we could look at in this loop. Yes, there is one more (or two more, depending on how you count it) step in this loop, which is the render phase.

