๐ก Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a
.htmlfile with<script>tags and open it in your browser
Advanced JavaScript โ Event Loop, Microtasks & Async Internals
What You'll Learn in This Lesson
- โCall Stack & Task Queues
- โMicrotasks vs Macrotasks priority
- โHow Promises bypass setTimeout
- โEvent Loop starvation bugs
- โBrowser rendering pipeline
- โNode.js vs Browser differences
๐ช Real-World Analogy: Single Juggler at a Circus
Imagine JavaScript as a single juggler who can only hold one ball at a time. When someone tosses them a new ball (like a network request), they hand it to an assistant (Web API) who holds it temporarily. When the assistant is done (request completes), they place the ball in a queue. The juggler only grabs from the queue when their hands are empty. This is why JavaScript is "non-blocking" โ the juggler never stops to wait; they keep juggling while assistants handle the waiting.
๐ Event Loop Priority Order:
| Priority | Queue Type | Examples | When Processed |
|---|---|---|---|
| 1st | Call Stack | Synchronous code | Immediately |
| 2nd | Microtask Queue | Promise.then, queueMicrotask | After current task, before render |
| 3rd | Macrotask Queue | setTimeout, events, fetch | After microtasks complete |
The JavaScript event loop is the hidden engine that powers asynchronous behaviour in browsers, Node.js, mobile runtimes, UI frameworks, and high-performance servers. Although JavaScript is single-threaded, it achieves concurrency using a sophisticated scheduling system built around the call stack, task queues, microtask queues, Web APIs, and the event loop cycle. Understanding this system is essential for writing non-blocking code, fixing race conditions, preventing UI freezes, and optimising back-end performance. Most bugs in async JavaScript come from not understanding when something executes โ not how it executes.
The event loop is designed around one rule: JavaScript never pauses the main thread. Instead, long-running operations (network requests, timers, DOM events, I/O, file operations) run in the background through browser/Node APIs. Once finished, they push callbacks into queues, which the event loop processes in a strict order.
๐ฅ Call Stack โ The Execution Engine
The call stack is a LIFO (Last In, First Out) structure. Each time a function executes, the engine creates a new execution context and pushes it onto the stack.
Call Stack Example
See how functions are pushed and popped from the call stack
function a() {
b();
}
function b() {
console.log("B");
}
a();Stack order: global() โ a() โ b() โ console.log()
When the stack is empty, the event loop may process queued callbacks. A blocked stack freezes everything โ this is why infinite loops freeze your browser.
๐ฅ Web APIs / Node APIs โ Async Work is Not Done by JavaScript
JavaScript itself cannot do asynchronous work. Instead, browsers have Web APIs and Node.js has C++ bindings and libuv threadpool.
Web APIs Example
See how setTimeout delegates work to browser APIs
setTimeout(() => console.log("Done"), 1000);setTimeout does not wait inside JavaScript. It is delegated to browser timers. When done, the callback is pushed to the macrotask / task queue.
๐ฅ Macrotask Queue (Task Queue) โ Timers, I/O, Rendering
Macrotasks (also called "tasks") include: setTimeout, setInterval, setImmediate (Node), requestAnimationFrame, DOM events, File I/O callbacks, Network callbacks, MessageChannel, Script parsing.
Macrotask Queue Example
See how setTimeout(0) still waits for the next loop cycle
console.log(1);
setTimeout(() => console.log(2), 0);
console.log(3);Output: 1 3 2. Even setTimeout(...,0) is never immediate โ it always waits for the next loop cycle.
๐ฅ Microtask Queue โ Higher Priority Than Everything Else
Microtasks include: Promise callbacks (.then, .catch, .finally), MutationObserver, queueMicrotask(), async/await continuations, some V8 internal jobs.
Microtasks run before any macrotasks โ and after each synchronous execution block.
Microtask Queue Example
See how Promise callbacks run before macrotasks
console.log("A");
Promise.resolve().then(() => console.log("B"));
console.log("C");Output: A C B. This is because microtasks run after the call stack empties but before any macrotasks.
๐ฅ Microtasks Can Starve the Event Loop
Because microtasks run before macrotasks, infinite microtask scheduling blocks timers and event handlers:
Microtask Starvation
See how infinite microtasks can starve the event loop
function loop() {
Promise.resolve().then(loop);
}
loop();
setTimeout(() => console.log("Timer fired"), 0);The timer never fires. This is called microtask starvation and is a real source of production bugs.
๐ฅ Event Loop Order โ The Golden Rule
Every cycle works like this:
- Execute everything in the call stack
- Execute the entire microtask queue (until empty)
- Render updates (browser only)
- Execute one macrotask
- Repeat forever
This priority system means: Promises always beat setTimeout, await resolves before timers, UI updates may be delayed if microtasks are heavy.
๐ฅ Promises vs setTimeout โ Why Promises Always Win
Promise vs setTimeout
See why Promise callbacks always run before setTimeout
setTimeout(() => console.log("Timeout"), 0);
Promise.resolve().then(() => console.log("Promise"));Output: Promise Timeout. Because Promise callbacks = microtasks, setTimeout = macrotask. Microtasks always run first.
๐ฅ Async/Await โ It's Just Syntactic Sugar Over Promises
Async/Await Sugar
See how await splits the function into two parts
async function test() {
console.log(1);
await null;
console.log(2);
}
test();
console.log(3);Output: 1 3 2. Because await splits function into two parts. The second part becomes a microtask.
๐ฅ Long Example: Understanding Full Async Flow
Full Async Flow
Understand the complete async execution order
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve()
.then(() => {
console.log("promise1");
return Promise.resolve();
})
.then(() => console.log("promise2"));
console.log("end");Execution order:
- "start" โ sync
- "end" โ sync
- Microtask queue: "promise1"
- Microtask queue: "promise2"
- Macrotask: "timeout"
Final output: start end promise1 promise2 timeout
๐ฅ The Major Async Queues of JavaScript
1. Microtask Queue (Highest Priority)
This queue instantly executes after every synchronous block. It contains: Promise reactions (then, catch, finally), queueMicrotask(), MutationObserver, Async/await continuation jobs.
Microtasks can "starve" the event loop โ meaning they can block macrotasks indefinitely if scheduled too aggressively.
queueMicrotask Starvation
See how queueMicrotask can starve the event loop
function loop() {
queueMicrotask(loop);
}
loop();
setTimeout(() => console.log("Timeout never runs"), 0);2. Animation Frame Queue (Browser Only)
This queue runs before the browser draws a new frame (ideally at 60fps). Used for: smooth animations, games, physics engines, canvas rendering, UI transitions.
requestAnimationFrame
See how rAF runs before the browser paints
requestAnimationFrame(() => {
console.log("Runs before the next frame is painted");
});3. Macrotask Queue (Lower Priority)
This includes: Timers (setTimeout, setInterval), UI events, I/O callbacks, MessageChannel, Script execution, Network callbacks (XHR), setImmediate (Node.js).
The event loop processes ONE macrotask per turn.
๐ฅ How Browsers Render Pages During the Event Loop
The browser executes tasks in this order:
- Run JavaScript
- Run all microtasks
- Render UI changes (paint, style recalc, layout)
- Run the next macro task
If microtasks run too long, the browser skips rendering cycles, causing: Lag, Frozen animations, Slow interactions, Delayed clicks, Janky scrolling.
๐ฅ How Node.js Differs From Browsers
Node.js uses libuv, which introduces more granular queues: Next Tick Queue (highest priority in Node), Promise/Microtask Queue, Timers Phase, Pending Callbacks, Idle/Prepare, Poll Phase, Check Phase (setImmediate), Close Callbacks.
Node.js vs Browser
See how async ordering differs between environments
setTimeout(() => console.log("Timeout"), 0);
setImmediate(() => console.log("Immediate"));
Promise.resolve().then(() => console.log("Microtask"));Output order varies between browser and Node. Promises always run before both.
๐ฅ Async Generators, Streams & the Event Loop
Async generators (async function*) create sequences of values pulled over time. Under the hood, each yield schedules a microtask before returning control to the caller.
Async Generators
See how async generators schedule microtasks between yields
async function* streamData() {
yield "A";
await new Promise(r => setTimeout(r, 0));
yield "B";
yield "C";
}
(async () => {
for await (const v of streamData()) {
console.log(v);
}
})();Output order: A B C. Microtasks schedule between each step. This model is used heavily in: live chat streams, video decoding pipelines, sensor feeds, AI inference batching, WebSocket data, game engines, backend log streams.
๐ฅ The Browser's Rendering Pipeline & The Event Loop
Each frame (ideally 16.6ms per frame) follows:
- Handle user input
- Run JavaScript
- Run microtasks
- Recalculate styles
- Recalculate layout
- Update layers
- Paint
- Commit frame to screen
If your JavaScript uses too many microtasks, the browser delays layout and painting. This causes: low FPS, stuttering animations, delayed click responses, broken transitions.
๐ฅ How requestIdleCallback() Helps
This function runs code only when: the browser is idle, CPU load is low, no urgent tasks exist.
requestIdleCallback
See how to run code when the browser is idle
requestIdleCallback(() => {
console.log("Browser is idle, running safe work");
});Great for: preloading assets, warm caching, indexing content, analytics batching, preparing UI transitions.
๐ฅ Network, Fetch & the Event Loop
Network requests (fetch) don't run inside JS. They run inside browser networking threads. When they finish, they schedule a macrotask.
Fetch & Event Loop
See how network requests interact with the event loop
console.log("Start");
fetch("url").then(() => console.log("Fetch done"));
console.log("End");Output: Start End Fetch done
๐ฅ Professional Strategy for Predictable Async Code
- Keep microtask chains short
- Offload heavy computation
- Prefer
requestAnimationFramefor visual updates - Prefer
requestIdleCallbackfor non-urgent tasks - Use Web Workers for anything CPU-heavy
- Log execution order when debugging
- Avoid deep Promise nesting
- Test on slow devices
- Remember Node and Browser ordering differ
- Never rely on timers for precise orchestration
๐ฅ Final Example โ Full Event Loop Ordering Breakdown
Full Event Loop Ordering
See the complete async system in action
console.log("Start");
setTimeout(() => console.log("Macrotask"), 0);
Promise.resolve()
.then(() => console.log("Microtask 1"))
.then(() => console.log("Microtask 2"));
requestAnimationFrame(() => console.log("Animation Frame"));
console.log("End");Expected output: Start End Microtask 1 Microtask 2 Animation Frame Macrotask
This single example demonstrates the entire async system.
๐ฏ Key Takeaways
- The event loop is the foundation of all async JavaScript behaviour
- Microtasks always execute before macrotasks
- Infinite microtasks can starve the event loop
- Understanding async internals is essential for building scalable applications
- Promises and async/await are syntactic sugar over the event loop
- Browser rendering depends on proper async task scheduling
Sign up for free to track which lessons you've completed and get learning reminders.