The event loop is the mechanism that lets single-threaded JavaScript perform non-blocking asynchronous work. It continuously moves callbacks from task queues onto the call stack when the stack is empty.
JavaScript runs on one thread with a call stack. Async callbacks wait in queues: the MACROTASK queue (setTimeout, setInterval, I/O, UI events) and the MICROTASK queue (Promise .then/.catch/.finally, queueMicrotask, async/await continuations). After each macrotask (and after the initial script), the engine drains the ENTIRE microtask queue before rendering or picking up the next macrotask. This means promises always resolve before the next setTimeout, even a setTimeout(fn, 0). Understanding this order explains subtle timing bugs and why a flood of microtasks can starve rendering.
A flaky UI test fails because an assertion runs before a promise-based state update. Knowing microtasks flush before timers explains why awaiting a microtick (await Promise.resolve()) or the framework's 'flush' helper fixes the ordering, while a setTimeout(0) hack is less reliable.
What is the output order? console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');