The JS Event Loop, Macrotasks, and Microtasks — Step by Step
Misreading async execution order is one of the most common sources of production bugs in JavaScript. Knowing that microtasks always drain before the next macrotask — and that `await` splits a function into synchronous and microtask halves — prevents race conditions and interview failures alike.
JavaScript runs on a single thread to avoid DOM rendering conflicts, so time-consuming operations get queued as either macrotasks (script, setTimeout, I/O) or microtasks (Promise.then, MutationObserver). The event loop follows a fixed four-step cycle: execute all synchronous code, drain every microtask in the queue, optionally render the page, then pick up the next macrotask.
Two details catch developers off guard. First, setTimeout callbacks don't execute in insertion order — the engine picks whichever timer expires first. Second, async/await is Promise syntax sugar: the code to the right of `await` runs synchronously, but everything after it gets pushed into the microtask queue.
A worked example traces eight console.log statements through a script mixing async functions, a zero-delay setTimeout, and chained Promise .then calls. The walkthrough shows exactly when each line prints and why the microtask queue must be completely empty before any macrotask runs.
The mental model of 'await splits the function' is more reliable than memorizing output orders, because it explains why `async1 end` prints before `then1` even though `async1()` was called first.
The fact that all microtasks must drain before any macrotask runs means a recursively added microtask can starve the macrotask queue and block rendering indefinitely.
Many developers assume `setTimeout(fn, 0)` runs immediately after sync code, but the microtask queue always jumps the line, which is why `then1` and `then2` print before `setTimeout` in the example.