How does the JavaScript event loop work?
Quick Answer
JavaScript runs on a single thread with a call stack. Async work (timers, I/O, promises) is handed to the host, and when it completes a callback is queued. The event loop waits for the stack to empty, then runs all microtasks (promise callbacks, queueMicrotask), then one macrotask (a timer, an event), then repeats — draining microtasks after every macrotask.
JavaScript is single-threaded — there's one call stack. When I call something async like setTimeout or fetch, the host environment handles the waiting, and when it's done it puts a callback on a queue. The event loop's job is: once the call stack is empty, run everything in the microtask queue — that's promise callbacks — then take exactly one task from the macrotask queue, like a timer callback, then drain microtasks again, and repeat. The practical takeaways are that a promise .then always beats a setTimeout(0) queued before it, and that any long synchronous function freezes the whole page because nothing else can run until the stack clears.
Pieces:
- Call stack — synchronous execution, one frame at a time.
- Web/Node APIs —
setTimeout,fetch, DOM events run outside the engine and schedule callbacks when done. - Macrotask (task) queue — timers, I/O, UI events,
MessageChannel. - Microtask queue — resolved
Promisecontinuations,queueMicrotask,MutationObserver. Higher priority.
Loop iteration: run sync code to completion → drain the entire microtask queue → take one macrotask → drain microtasks again → (render) → repeat.
Why it matters: a promise .then always runs before a setTimeout(…, 0) scheduled earlier. Long synchronous work blocks everything, including rendering — that's why you break up heavy loops or move work to a Web Worker.
console.log('1');
setTimeout(() => console.log('2 (macrotask)'), 0);
Promise.resolve().then(() => console.log('3 (microtask)'));
console.log('4');
// output: 1, 4, 3 (microtask), 2 (macrotask)