Event Loop
The event loop is the mechanism that lets single-threaded JavaScript handle asynchronous work by continuously taking queued callbacks — microtasks like Promises first, then macrotasks like timers — and running them once the call stack is empty.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
🎪 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.
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.
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 .
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.
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.
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:
The timer never fires. This is called microtask starvation and is a real source of production bugs.
🔥 Event Loop Order — The Golden Rule
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
Output: Promise Timeout . Because Promise callbacks = microtasks, setTimeout = macrotask. Microtasks always run first.
🔥 Async/Await — It's Just Syntactic Sugar Over Promises
Output: 1 3 2 . Because await splits function into two parts. The second part becomes a microtask.
🔥 Long Example: Understanding Full Async Flow
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.
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.
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
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.
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.
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:
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.
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.
🔥 Professional Strategy for Predictable Async Code
🔥 Final Example — Full Event Loop Ordering Breakdown
Expected output: Start End Microtask 1 Microtask 2 Animation Frame Macrotask
This single example demonstrates the entire async system.
🎯 Key Takeaways
Practice quiz
What is the call stack's structure?
- FIFO (First In, First Out)
- A priority queue
- LIFO (Last In, First Out)
- A circular buffer
Answer: LIFO (Last In, First Out). The call stack is a LIFO structure; the most recently pushed execution context runs and pops first.
Between microtasks and macrotasks, which runs first?
- Microtasks
- Macrotasks
- They alternate one-for-one
- Whichever was scheduled first
Answer: Microtasks. The event loop drains the entire microtask queue before processing any macrotask.
Which of these is a microtask?
- setTimeout callback
- setInterval callback
- A DOM click event
- Promise.then callback
Answer: Promise.then callback. Promise reactions (.then/.catch/.finally) and queueMicrotask are microtasks; timers and events are macrotasks.
Which is a macrotask?
- queueMicrotask()
- setTimeout
- Promise.then
- await continuation
Answer: setTimeout. setTimeout, setInterval, I/O and events are macrotasks; promise callbacks are microtasks.
What does this log: console.log(1); setTimeout(() => console.log(2), 0); console.log(3);
- 1 3 2
- 1 2 3
- 2 1 3
- 3 1 2
Answer: 1 3 2. setTimeout(...,0) still waits for the next loop cycle, so the order is 1, 3, 2.
What does this log: console.log('A'); Promise.resolve().then(() => console.log('B')); console.log('C');
- A B C
- B A C
- A C B
- C A B
Answer: A C B. B is a microtask, so it runs after the synchronous A and C: A, C, B.
Why does setTimeout(fn, 0) never run truly immediately?
- The browser ignores 0
- It is delegated to a timer and queued as a macrotask for the next loop cycle
- Promises block it
- 0 means 1 second
Answer: It is delegated to a timer and queued as a macrotask for the next loop cycle. setTimeout hands work to the environment's timers and the callback is queued as a macrotask for a later cycle.
What is 'microtask starvation'?
- Running out of memory
- A slow promise
- Too many setTimeout calls
- Infinitely scheduling microtasks so macrotasks (like timers) never get to run
Answer: Infinitely scheduling microtasks so macrotasks (like timers) never get to run. If microtasks keep scheduling more microtasks, the loop never reaches the macrotask queue, so timers never fire.
What does this log: async function test(){ console.log(1); await null; console.log(2); } test(); console.log(3);
- 1 2 3
- 1 3 2
- 3 1 2
- 1 2 then 3
Answer: 1 3 2. await splits the function; the code after await becomes a microtask, so it logs 1, 3, 2.
Full flow: console.log('start'); setTimeout(t); Promise.resolve().then(p1).then(p2); console.log('end'); What order do the labels print?
- start end timeout promise1 promise2
- start promise1 promise2 end timeout
- start end promise1 promise2 timeout
- start end promise1 timeout promise2
Answer: start end promise1 promise2 timeout. Sync first (start, end), then all microtasks (promise1, promise2), then the macrotask (timeout).