Basic Docs
javascriptasync

JavaScript Event Loop

How async JavaScript works under the hood with the event loop, call stack, and task queues.

What is the Event Loop?

JavaScript is single-threaded — only one thing runs at a time. The Event Loop is the mechanism that allows non-blocking async operations by coordinating the Call Stack, Web APIs, and task queues.

Call Stack
Web APIs
Callback Queue
Event Loop
Call Stack
Event Loop Cycle
1
Call Stack
Runs synchronous code
2
Web APIs
Handles async work offloaded from stack
3
Task Queue
Stores completed callbacks
4
Event Loop
Checks if stack is empty
5
Call Stack
Callback pushed and executes

When async work (timers, network calls) completes in Web APIs, their callbacks are pushed into the Callback Queue. The Event Loop moves them onto the Call Stack only when the stack is empty.

Microtasks vs Macrotasks

Not all async callbacks are equal. The event loop processes two distinct queues with different priorities.

MicrotasksMacrotasks
Promise.then / catch / finallysetTimeout / setInterval
queueMicrotask()MessageChannel
MutationObserverrequestAnimationFrame
Runs before next macrotaskRuns after all microtasks clear
Drains the entire queue each tickOne task per event loop tick
Execution Priority (highest first)
Synchronous code100
Microtask (Promise.then)75
requestAnimationFrame50
Macrotask (setTimeout)25
Key rule
The entire microtask queue runs to completion after every task — including after each macrotask. Microtasks always take priority.

Execution Order

Understanding the exact order helps you reason about async code. The steps below describe one full tick of the event loop.

1
Synchronous code runs first
All synchronous statements execute top-to-bottom on the Call Stack. Nothing async runs yet.
2
Async callbacks are offloaded
setTimeout, fetch, and event listeners are handed to Web APIs. The call stack is freed immediately.
3
Microtask queue drains
When the stack empties, every pending microtask (Promise callbacks) runs to completion before any macrotask.
4
One macrotask executes
The event loop picks the oldest macrotask from the queue (e.g. a setTimeout callback) and runs it.
5
Repeat
After each macrotask, the microtask queue drains again. This cycle continues until both queues are empty.
Execution order example
console.log("1 - start");

setTimeout(() => console.log("2 - setTimeout"), 0);

Promise.resolve().then(() => console.log("3 - promise"));

console.log("4 - end");

// Output:
// 1 - start
// 4 - end
// 3 - promise   ← microtask runs first
// 2 - setTimeout ← macrotask runs last

Common Pitfalls

!Blocking the call stack
Heavy synchronous work (large loops, synchronous XHR) blocks the entire thread. No UI updates, no event handling, no async callbacks can run while the stack is busy.
!Infinite microtask loop
Recursively scheduling microtasks (e.g. calling queueMicrotask inside a microtask) starves the macrotask queue and freezes the page — the loop never advances to the next tick.
isetTimeout(fn, 0) is not instant
A delay of 0 ms is clamped by browsers (typically to 1–4 ms) and still runs after all pending microtasks. It is not equivalent to synchronous execution.
Built: 4/8/2026, 12:01:11 PM