← all posts

The event loop is not magic

·2 min read ·JavaScript · Computer Science · Performance

JavaScript is single-threaded. One call stack, one thing at a time. And yet it handles network requests, user interactions, and timers without blocking. Understanding how is one of the most useful things you can know as a JavaScript developer.

The Call Stack

When you call a function, it goes on the call stack. When it returns, it comes off. Synchronous code runs in order, blocking everything else until it's done.

The Task Queue

When a setTimeout expires or an event fires, the callback doesn't immediately run. It goes into the task queue. The event loop watches the call stack—when it's empty, it picks the next task from the queue and runs it.

console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Output: 1, 3, 2

setTimeout(fn, 0) doesn't mean 'run immediately.' It means 'run after the current call stack is empty.' That's why 2 prints after 3.

The Microtask Queue

Promises resolve differently from timers. Resolved Promise callbacks go into the microtask queue, which is processed after every task—before the next task from the task queue runs.

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2

Microtasks (Promise callbacks, queueMicrotask) run before the next setTimeout callback, even if both are ready.

Why This Matters in Practice

This explains why awaiting a resolved promise still yields to other pending microtasks. It explains why you can use setTimeout(fn, 0) to defer work until after the current synchronous block. It explains why a long synchronous loop blocks the UI.

You don't need to think about this constantly. But when you encounter the behavior that surprises you, having the mental model makes debugging take minutes instead of hours.