The event loop
JavaScript runs on a single thread, but your programs still handle timers, network responses, and DOM events without freezing the page. The event loop is the scheduler that moves work between the call stack, Web APIs, and task queues.
Runtime at a glance
The diagram below is a simplified map of how execution flows: synchronous work fills the call stack, asynchronous work is handled by host APIs, and the event loop decides what runs next.
Interactive runtime map
Idle: the call stack is empty and task queues have no pending work yet.
Queues & scheduling
When the call stack is empty, the event loop pulls the next task. Two queue families matter in practice:
- Macrotasks —
setTimeout,setInterval, I/O callbacks, UI rendering work. - Microtasks — Promises,
queueMicrotask, and some internal jobs run before the next macrotask.
That ordering is why promise callbacks can run before the next timer, even if the timer was scheduled first.
Microtasks vs macrotasks
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')
The console prints A, D, C, then B. Synchronous code finishes first, microtasks
drain next, and the macrotask queue runs after.
Non-blocking I/O
Non-blocking I/O
Network and disk work are delegated to the host. When results arrive, callbacks are queued—your main thread keeps parsing and rendering.
Task prioritization
Microtasks starve macrotasks if enqueued in a loop—always yield for long chains. Browsers may also interleave rendering for responsiveness.
Takeaways
- The call stack runs synchronous JavaScript; when it empties, the event loop can pick new tasks.
- Microtasks run to completion before the next macrotask.
- Timers are not guarantees of exact timing—they schedule macrotasks.
✦Remember: the loop is cooperative. Long synchronous work still blocks rendering and input—profile hot paths and split work with
queueMicrotask,requestAnimationFrame, or chunked scheduling when needed.