JavaScript has a single thread of execution. One thing at a time, in order, with no real parallelism.
And yet: you fire a fetch, the rest of the code keeps running, and when the response arrives the callback runs. You open a page with animations, timers and click events all working at the same time. How?
The answer is the event loop.
The pieces it is made of
The event loop is not one thing — it is the coordination between several:
Call Stack — the execution stack. When you call a function, it gets pushed. When it finishes, it gets popped. JavaScript executes whatever is on top of the stack. If the stack has something on it, the thread is busy.
Web APIs — services provided by the browser (or by Node.js) outside the main thread. setTimeout, fetch, DOM events — when you call them, the task is handed off to these APIs and the thread is free to carry on.
Task Queue (macrotasks) — when a Web API finishes its work, it puts the callback in this queue. setTimeout, setInterval and DOM events go here.
Microtask Queue — a queue with higher priority than the Task Queue. Promises (.then, .catch, async/await) and MutationObserver go here. It is drained completely before the event loop picks up the next macrotask.
The event loop itself does one single thing, in a continuous cycle: if the Call Stack is empty, take the next item from the queue and run it.

The ordering that explains everything
console.log('Start')
setTimeout(() => {
console.log('Timeout')
}, 0)
Promise.resolve().then(() => {
console.log('Promise')
})
console.log('End')
Result:
Start
End
Promise
Timeout
Why:
console.log('Start')— goes on the stack, runs, gets poppedsetTimeout(..., 0)— handed off to the timer Web API; the callback is queued in the Task Queue when it finishes (immediately, but still afterwards)Promise.resolve().then(...)— the callback is queued in the Microtask Queueconsole.log('End')— goes on the stack, runs, gets popped- Stack empty → the event loop drains the Microtask Queue first → runs
'Promise' - Stack empty, Microtask Queue empty → takes the next macrotask → runs
'Timeout'
setTimeout(..., 0) does not mean "run this now". It means "run this as soon as possible after the stack is empty and the microtask queue is drained". The difference matters.
Microtasks vs macrotasks
The distinction between the two queues is not arbitrary. Microtasks get priority because they are designed for immediate reactions to the current state — a Promise's .then has to run before the browser paints the screen or processes the next event.
Promise.resolve()
.then(() => {
console.log('microtask 1')
return Promise.resolve()
})
.then(() => console.log('microtask 2'))
setTimeout(() => console.log('macrotask'), 0)
// microtask 1
// microtask 2
// macrotask
Every chained microtask runs before the event loop processes the next macrotask. If a microtask queues another microtask, that one also runs before the macrotasks.
Why a blocked stack freezes everything
If a synchronous operation takes a long time — a loop chewing through an array of a million elements, a heavy encryption function — it blocks the call stack. As long as the stack has something on it, the event loop cannot process anything from the queues.
// This blocks the thread for as long as it takes
function procesarMillon() {
for (let i = 0; i < 1_000_000; i++) {
// heavy computation
}
}
procesarMillon() // UI frozen, events unresponsive, timers not firing
The UI stops responding. Clicks are not processed. Timers fall behind. Everything waits for the stack to empty.
That is why CPU-intensive operations belong in Web Workers — to run them on a separate thread without blocking the event loop.
How to debug timing problems
Chrome DevTools → Sources → Call Stack shows you what is on the stack at any breakpoint. If you see callbacks stacked up in unexpected ways, that is your problem right there.
Performance Profiler records an execution timeline. Long blocks on the main thread (long tasks) are tasks that blocked the event loop for more than 50ms — the metric Chrome uses to flag jank.
Strategic console.log is still the fastest way to confirm execution order when something does not happen when you expect it to.
What the event loop does not solve
The event loop makes asynchronous concurrency possible — multiple I/O operations in flight at the same time. It does not give you real parallelism.
If you need to run code in parallel to take advantage of multiple cores, you need Workers. The event loop handles "many things waiting"; Workers handle "many things running".
Understanding the difference between those two models is what separates knowing how to use async/await from understanding why it works.