
JavaScript Event Loop Explained: The Real Difference Between Microtasks and Macrotask
Muhammad Kamran
Did you work with JavaScript and often face issues like a code block running before another, and you don't understand why? Yeah, I've been there too. Spent hours debugging something that should've worked, only to realize I didn't understand the Event Loop properly.
Here's the thing - JavaScript has three main players: the call stack, microtask queue, and macrotask queue. Sounds complicated? It's actually pretty simple once someone explains it without all the fancy terminology.
The Call Stack - Your Regular Code
First, let's talk about the call stack. This is where your regular, everyday code runs. It's synchronous, meaning it goes line by line.
console.log('First');
console.log('Second');
console.log('Third');
// Output:
// First
// Second
// Third
Nothing crazy here. Top to bottom, just like you'd expect.
until you use setTimeout
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
// Output:
// Start
// End
// Timeout
Hold up. I set the timer to ZERO milliseconds. Why did it run last?
This is where the macrotask queue comes in. Things like setTimeout, setInterval, and other async stuff go into this queue. They wait until the call stack is empty before running. Even if you set the delay to 0.
But Wait, There's Another Queue?
Yep. The microtask queue. And it's basically the VIP line.
Promises live here. And they cut in front of everything in the macrotask queue.
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
// Output:
// Start
// End
// Promise
// Timeout
See? Promise ran before setTimeout, even though I wrote setTimeout first. That messed with my head for a while.
How This Actually Works
Okay, so here's what JavaScript does:
Runs all your normal code (call stack)
Looks at the microtask queue - runs EVERYTHING there
Takes ONE thing from the macrotask queue
Checks microtasks again
Repeats
Microtasks are greedy. They all run together. Macrotasks? One at a time.
Let me show you something that will really helped you to understand:
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
setTimeout(() => {
console.log('setTimeout 2');
Promise.resolve().then(() => {
console.log('Promise inside setTimeout');
});
}, 0);
console.log('Script end');
// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout 1
// setTimeout 2
// Promise inside setTimeout
What happened here:
Both console.logs ran first (normal code)
Then BOTH promises ran together (microtask queue)
Then first setTimeout ran (macrotask queue, one at a time)
Then second setTimeout ran
Then the promise inside that setTimeout ran (back to microtasks)
async/await Makes This Trickier
Check this out:
console.log('Start');
async function example() {
console.log('Inside async function');
await Promise.resolve();
console.log('After await');
}
example();
console.log('End');
// Output:
// Start
// Inside async function
// End
// After await
Everything before await runs normally. But everything after? That becomes a microtask. So it waits.
But you may think, what is meant by it becomes a microtask queue? It means code after await in a function will go to the microtask queue. Then the code outside the function will run normally until synchronous code runs completely. Then, this microtask queue code will run.
What we Learned
Here's what actually stuck with me:
Synchronous code always goes first.
Promises run before setTimeout, setInterval, and other timer stuff.
Everything after await becomes a microtask. Plan accordingly.
Never block the main thread with heavy loops or calculations. It freezes everything.
Enjoyed this article?
Check out more of my content or get in touch if you'd like to work together on your next project.