Microtasks and Macrotasks: The Secret to Mastering the JavaScript Event Loop
JavaScript runs on a single thread — so why is it so good at handling async work? The answer is the Event Loop. But to truly understand why one line runs before another, you need to know the difference between Microtasks and Macrotasks — two queues with completely different priorities.

Trung Vũ Hoàng
Author
1. How Does the Event Loop Work?
JavaScript executes code in the Call Stack. When it encounters async work (setTimeout, fetch...), it offloads it and continues running synchronous code. When async operations finish, their results aren’t pushed straight into the Call Stack — they’re placed into a queue. The Event Loop checks: if the Call Stack is empty, it pulls a task from the queue and executes it.
The key point is that there are two types of queues with completely different priorities: the Microtask Queue and the Macrotask Queue.
2. Microtask — Highest Priority
A Microtask is a small task that runs immediately after the Call Stack becomes empty — before any Macrotasks are processed. The Event Loop will drain the entire Microtask Queue before moving on.
Sources of Microtasks:
Promise callbacks:
.then(),.catch(),.finally()queueMicrotask()— pushes directly into the microtask queueMutationObserver— watches DOM changes
Warning: If a Microtask creates new Microtasks, those will also run immediately. Creating an infinite Microtask loop will block the Event Loop, preventing the browser from rendering UI or handling user input.
3. Macrotask — Patiently Waiting in Line
Macrotasks (or “Tasks”) are larger system-level tasks. The Event Loop pulls only one Macrotask per tick — then it must drain the Microtask Queue before pulling the next Macrotask.
Sources of Macrotasks:
setTimeout()/setInterval()setImmediate()(Node.js)I/O events: mouse clicks, key presses, incoming network data
Rendering: the browser re-paints the UI
4. Execution Order — The Golden Rule
Run all synchronous code in the Call Stack.
Drain the entire Microtask Queue.
Execute one Macrotask.
Go back to step 2 (check for new Microtasks).
Repeat.
5. The “Legendary” Example That Shows Up in Every Interview
console.log('1 - Synchronous');
setTimeout(() => {
console.log('2 - Macrotask');
}, 0);
Promise.resolve().then(() => {
console.log('3 - Microtask (Promise)');
});
queueMicrotask(() => {
console.log('4 - Microtask (Explicit)');
});
console.log('5 - Final synchronous');Output:
1 - Synchronous
5 - Final synchronous
3 - Microtask (Promise)
4 - Microtask (Explicit)
2 - MacrotaskIf you understand why 5 prints before 3, and why 3 prints before 2 — you already understand 80% of how the Event Loop works.
6. Real-World Use Cases and Performance Notes
Split heavy work so the UI doesn’t freeze
If a computation takes 1 second, don’t put it in a Promise. Use setTimeout to chunk it — allowing the browser to interleave rendering between tasks:
function processChunk(data, index) {
if (index >= data.length) return;
// Process one part
processItem(data[index]);
// Yield to the browser to render, then continue
setTimeout(() => processChunk(data, index + 1), 0);
}Guarantee ordering after state changes
Use queueMicrotask() when you need an action to run right after a state update but before any Macrotask runs — useful in reactive libraries.
Avoid Microtask Hell
Recursion inside a Promise can block the Macrotask Queue forever — your app won’t be able to respond to user clicks or scrolling anymore.
Conclusion
Understanding Microtasks and Macrotasks is the step from coder to a real JavaScript engineer:
Microtask → runs immediately after each synchronous task, with absolute priority.
Macrotask → waits in line, only one task per Event Loop tick.
Order: Sync → Microtask → Macrotask → Microtask → Macrotask → ...
This knowledge is foundational for debugging timing issues in React, Node.js, and any JavaScript framework.
Frequently Asked Questions
Bài viết liên quan

Zustand Async: 5 Effective Ways to Handle Async in React
Every real-world React app needs to communicate with async APIs. If handled poorly, it’s easy to run into issues like frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an extremely simple syntax—define async actions directly in the store, without complex middleware like Redux Thunk or Saga.

Advanced Promises: all, allSettled, race, any — When to Use Which?
If you only use sequential async/await, your app is wasting performance potential — each request has to wait for the previous one to finish before it can start. Promise’s static methods (all, allSettled, race, any) let you orchestrate multiple async tasks in parallel using the strategy that fits each problem.

Zustand Async: 5 Effective Ways to Handle Async Operations in React
Every real-world React app has to communicate with asynchronous APIs. If managed poorly, your app can run into issues like a frozen UI, race conditions, memory leaks, or showing stale data. Zustand solves this with an incredibly simple syntax — define async actions directly in the store, without complex middleware like Redux Thunk or Saga.