The JavaScript Event Loop, Explained Without the Jargon
3 min read
If you've ever stared at a setTimeout that fired "late," or a Promise that resolved in
an order you didn't expect, you've met the event loop — you just weren't formally
introduced. Most explanations bury it under spec language. Here's the version I wish
someone had given me fifteen years ago.
What the event loop actually is
JavaScript runs your code on a single thread. One thing at a time, always. The event loop is the mechanism that makes a single-threaded language feel like it's doing ten things at once: it's a coordinator that decides what runs next once the current piece of work finishes.
That's the whole idea. Everything else is detail about the queues it coordinates.
The call stack: a single chef in a kitchen
Picture a kitchen with exactly one chef. Every function you call is an order ticket. The chef works one ticket at a time, start to finish — no pausing halfway to start another. That pile of in-progress tickets is the call stack: call a function, a ticket goes on top; the function returns, the ticket comes off.
The important consequence: while the chef is busy, nothing else happens. No clicks are handled, no renders occur. If one ticket takes ten seconds, the whole restaurant waits.
function chopOnions() {
/* ... */
}
function makeSoup() {
chopOnions(); // stack: makeSoup → chopOnions
}
makeSoup(); // one ticket at a time, until the stack is emptyWeb APIs: the helpers in the back room
So how does setTimeout work if there's only one chef? Because the timer isn't the
chef's job. When you call setTimeout, fetch, or add an event listener, the browser
(or Node) hands that waiting work to its own machinery — the back room. The chef moves on
immediately.
When the timer finishes or the response arrives, the back room doesn't interrupt the chef. It writes a new ticket and puts it in a queue.
The task queue and the microtask queue
Here's where the interesting bugs live. There are (at least) two queues:
- The task queue (or macrotask queue): callbacks from
setTimeout,setInterval, and most events. - The microtask queue: Promise callbacks (
.then,awaitcontinuations) andqueueMicrotask.
The event loop's rule is strict: when the call stack empties, drain the entire microtask queue first, then take one task from the task queue, then drain microtasks again, and so on. Promises always cut in line ahead of timers.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2 — the microtask (3) beats the "0ms" timer (2)That setTimeout(fn, 0) doesn't mean "run now." It means "queue this as a task" — and it
still waits for the current code and all microtasks to finish.
A real bug this explains
A classic support ticket: "My click handler fires out of order." The setup was a button
whose handler kicked off a data refresh, and a "loading" flag that got set inside a
.then():
button.addEventListener('click', () => {
refreshData().then(() => {
isLoading = false; // microtask — runs later
});
isLoading = true;
updateSpinner(); // reads isLoading = true, correct...
});The bug appeared when someone "optimized" a synchronous cache path to return a resolved
Promise. Developers assumed the .then() ran immediately for cached data — it doesn't.
Even an already-resolved Promise schedules its callback as a microtask, so isLoading = true and the spinner update always run first, and the spinner flashed on every cached
hit. Once you know microtasks always wait for the current stack to finish, the behavior
is obvious instead of haunted.
Key takeaways
- JavaScript is single-threaded; the event loop schedules what runs when the stack is empty.
setTimeout(fn, 0)queues a task; it never runs immediately.- Promise callbacks are microtasks and run before the next task — even for already-resolved Promises.
- Long synchronous work blocks everything: input, rendering, timers. (When you genuinely need heavy computation, that's what Web Workers are for.)
- When async order surprises you, ask one question: which queue is this callback in?