The Heartbeat of JS: Understanding the Event Loop (2026)
JavaScript is single-threaded, yet it fetches data, runs timers, and streams chat responses without freezing the page. Every junior developer is told this; few are told how. The answer is the event loop, and once you can see it, half of async JavaScript stops being superstition.
Where your code actually lives
Three places, constantly handing work to each other. The call stack is where synchronous code runs, one frame at a time, last in first out — while it is busy, nothing else on the main thread happens. The Web APIs are the browser's back kitchen: setTimeout, fetch, and DOM events are taken off your hands and timed or networked in the background. The callback queue is the pick-up counter where finished callbacks wait their turn. Node.js runs the same design through libuv, its loop polling for I/O and running ready callbacks on the main thread.
The loop itself asks two questions forever: is the stack empty, and is a callback waiting? Only when both answers align does a task get pushed on. That is why setTimeout(fn, 0) never runs immediately — zero milliseconds is a queue position, not a starting gun.
Microtasks jump the queue
Promises do not wait in the regular line. .then callbacks, await continuations, and queueMicrotask go to the microtask queue, and the loop drains it completely after every macrotask — before the next timer, before rendering. Predict the output before you run it:
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2
The sharp edge: an infinite microtask loop starves everything — rendering, timers, input — and the tab hangs. Respect the queue.
Do not block the loop
A long synchronous loop is the classic footgun: the page cannot paint, clicks go dead, and the browser offers to close the tab. Break the work up and hand control back periodically. scheduler.yield(), the successor to scheduler.postTask, is now baseline in Chrome, Edge, and Safari:
async function processItems(items) {
for (const item of items) {
doExpensiveWork(item);
await scheduler.yield(); // let the browser paint and take input
}
}
For genuinely heavy computation, leave the main thread altogether — Web Workers run real parallel threads and message results back home:
const worker = new Worker("./heavy.js", { type: "module" });
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => updateUI(e.data);
And for token streams from LLM endpoints or large file reads, the async iterator rides the same microtask machinery with cleaner syntax:
const response = await fetch("/api/chat");
for await (const chunk of response.body) {
appendToUi(chunk);
}
Coffee-shop version, since it sticks: you order, take a buzzer, and sit down; the cashier serves the next customer; your name is called only when the counter is free; and the VIP line of pending promises always goes first.
The loop's whole philosophy is waiting without freezing — patience as an engineering discipline. It puts me in mind of students in Palestine studying through connectivity that arrives in fragments, still keeping their place in the queue.
When an airline cancels a flight at 2 AM, our desk at HTG Travels runs a very human loop: check the queue, rebook, retry, confirm — until a family from Sialkot or Lahore is holding a real ticket rather than a promise. The waiting is our job, so it stops being yours.




