The Event Loop And Microtasks
Way back in post 1, a buzzer stood in for how JavaScript handles waiting. This is the actual kitchen behind that buzzer.
Way back in post 1, a buzzer stood in for how JavaScript handles waiting without freezing everything else. This is the actual kitchen behind that buzzer, the real machinery every setTimeout, every fetch, every Promise in this series has been running on the whole time.
One Chef, No Multitasking
JavaScript's engine runs on a single thread, one call stack, one heap for storing objects in memory. There is exactly one chef in this kitchen, and that chef can only do one thing at a time. No multitasking, ever, inside the engine itself. Languages like Java can run multiple things in parallel; JavaScript cannot.
That single-threaded constraint is exactly the problem post 1 opened with, a frozen cashier blocking an entire line. The rest of this post is how JavaScript avoids that without ever giving the engine a second thread.
The Web APIs Environment Is Where the Waiting Happens
The DOM, timers, fetch, geolocation, none of that is actually part of the JavaScript language. They're APIs the browser provides alongside the engine, running in what's called the Web APIs environment, external prep stations the one chef can hand slow work off to.
Set an image's src, and the actual download happens out there, not on the call stack. Start a fetch, same thing. That's the real answer to "where does asynchronous code actually run": not in the engine at all, in the browser's own environment around it.
The Callback Queue Is an Ordered Ticket Rail
Once a Web API task finishes, its callback doesn't go straight to the chef. It lands in the callback queue first, an ordered line, first in, first out, holding every callback that's ready to run and just waiting its turn.
Click events work the same way. A DOM click isn't asynchronous in the sense that anything is running in the background waiting, but the click handler's callback still goes through this exact same queue once the click fires.
The Event Loop Is the Host Checking If the Chef Is Free
The event loop's job is small and constant: look at the call stack, and if it's empty, pull the next callback off the callback queue and hand it to the chef. Each time that handoff happens is called a tick.
Worth noticing what this means for the engine itself: the chef has no sense of time. It just cooks whatever's put in front of it, one order at a time. Tracking delays, deciding what's ready, and choosing what runs next is the host's job, the event loop's, not the engine's.
That single mechanic has a real consequence worth internalizing: a timer's delay is a minimum, never a guarantee. A setTimeout set for a delay of 0 still has to wait in line behind whatever's already queued, and behind whatever the call stack is still busy running.
Hit Run. Even once the timer itself is done, its callback still has to sit in the callback queue and wait for a tick, and no tick can happen until that 200ms blocking loop finishes and the call stack actually empties out. 0 was never a promise about timing, only a minimum.
The Microtask Queue: Promises Get a Fast Pass
Promise callbacks, the functions passed to .then(), .catch(), .finally(), don't go into the regular callback queue at all. They get their own line: the microtask queue, and it has priority over the regular callback queue, every single time.
Run that and the order is Start, End, Promise callback, Timer callback, in that exact sequence, every time. At the end of each tick, before pulling the next regular callback, the event loop drains the entire microtask queue first. If a microtask itself queues another microtask, that one also jumps ahead of anything waiting in the regular queue.
One honest edge case worth knowing rather than demonstrating live: an endless chain of microtasks queuing more microtasks can starve the regular callback queue completely, nothing in it would ever get a turn. Not something that comes up in ordinary code, but a fair interview question, and now an answerable one.
Step through the exact same four lines below, one event loop tick at a time, and watch the call stack, the Web APIs environment, both queues, and the console output update as each line actually runs.
Event Loop Visualizer
console.log('Start');
setTimeout(() => console.log('Timer callback'), 0);
Promise.resolve().then(() => console.log('Promise callback'));
console.log('End');Call Stack
Web APIs Environment
Microtask Queue
Callback Queue
Console Output
Script starts. The global execution context goes on the call stack.
The Essentials
- The engine has exactly one thread, the call stack, and can only run one thing at a time.
- Asynchronous tasks (timers, DOM,
fetch) run in the browser's Web APIs environment, not inside the engine itself. - A finished task's callback waits in the callback queue, an ordered line, until the event loop hands it to the call stack.
- The event loop's whole job is checking whether the call stack is empty, then pulling the next callback if it is. Each handoff is a tick.
- A timer's delay is a minimum, not a guarantee, it still waits behind whatever the call stack is busy running.
- Promise callbacks go to a separate microtask queue that always runs first, draining completely before the event loop touches the regular callback queue again.
Keep reading