Architecture
The Event Loop
A single thread stays responsive by never waiting on I/O — it hands async work off, then a loop delivers the results back in a strict order: sync code, then microtasks, then macrotasks.
One thread. Block it and everything stops.
JavaScript (and many async runtimes) run your code on a single thread. A long synchronous task doesn't just delay itself — it freezes the UI, stalls every pending request, and blocks every other callback waiting to run.
function heavySync() {
let x = 0;
for (let i = 0; i < 5e9; i++) x += i;
// ← one thread, nothing else runs
return x;
}
button.onclick = heavySync; // UI freezes
Stack, queues, and a loop that never blocks
The Call Stack runs synchronous code. Async work (timers, network, I/O) is handed to the runtime; when it completes, its callback is queued — never run immediately. Once the stack is empty, the Event Loop pulls the next callback. Microtasks (Promises) always drain completely before the next macrotask (timers, I/O).
Step through the real execution order
Click Tick ▶ to advance the loop one step and watch items move between the Stack, the Microtask Queue, and the Task Queue.
console.log(1);
setTimeout(() => log(3));
Promise.resolve().then(() => log(2));
console.log('1b');
Concurrency without threads
- Single thread + non-blocking I/O = concurrency without the cost of threads.
- Never block the loop — chunk heavy work or offload it to workers.
- Order matters: all microtasks (Promises) drain before the next macrotask (timers, I/O).
- Everywhere: this model underlies Node.js and every browser's JavaScript runtime.