JavaScript runs on a single thread — one call stack, one thing executing at a time — which raises an obvious question: how does async code, like a fetch request or a setTimeout callback, run without freezing everything else on the page while it waits? The answer is the event loop, and it depends on three distinct pieces working together, not one.

The call stack is where currently-executing code actually runs, one function frame at a time, the same as in any single-threaded language. What's specific to JavaScript's model is what happens when that code calls something asynchronous, like setTimeout or a fetch: the browser (or Node's runtime) hands that operation off to be handled outside the call stack entirely, and the calling code keeps running immediately rather than waiting.

When that handed-off operation finishes — the timer elapses, the network response arrives — its callback doesn't run immediately, even if the call stack is empty. It gets placed in a queue instead: the task queue (also called the macrotask queue) for things like setTimeout and DOM events, or the microtask queue for promise callbacks and queueMicrotask.

The event loop's actual job is simple to state and easy to underestimate: continuously check whether the call stack is empty, and if it is, pull the next item off a queue and run it. That's the entire mechanism. It's why a setTimeout(fn, 0) callback still doesn't run instantly — it has to wait for the current call stack to fully empty first, no matter how short the delay is.

The distinction between the two queues matters in practice: the event loop always drains the entire microtask queue before it processes even one task from the (macro)task queue. That's why a chain of .then() callbacks on a promise can run to completion before a setTimeout callback fires, even if the setTimeout was scheduled first — promise callbacks jump the line ahead of the next macrotask, every time.

None of this requires multiple threads or true parallelism — it's cooperative scheduling on a single thread, coordinated through queues. MDN's own JavaScript documentation covers the event loop, along with the specific ordering guarantees between microtasks and macrotasks, in more depth than fits here.