JavaScript is single-threaded with one call stack — the event loop enables async behavior by processing microtasks (promises) before macrotasks (setTimeout) between each cycle of the call stack.
JavaScript has one call stack executing one piece of code at a time — async behavior is achieved through the event loop, not parallel execution.
Synchronous code runs on the call stack; async operations (timers, fetch, events) are handled by browser/Node APIs and their callbacks are queued.
Microtasks (promises, queueMicrotask) have priority over macrotasks (setTimeout, I/O). The entire microtask queue drains before the next macrotask runs.
Execute call stack → drain all microtasks → render if needed → pick one macrotask → repeat. This cycle runs continuously.
Runs after microtasks but before paint, synchronized with display refresh rate — the correct API for visual animations.
JavaScript runs on a single thread with one call stack, yet it handles thousands of concurrent operations — network requests, timers, user interactions — without blocking. The event loop is the mechanism that makes this possible, and understanding it is one of the most important (and most tested) JavaScript concepts.
The call stack is a LIFO (Last-In, First-Out) data structure that tracks function execution. When a function is called, a new frame is pushed onto the stack containing its execution context (local variables, this binding, return address). When the function returns, its frame is popped off. JavaScript executes code by processing whatever is on top of the stack — only one function runs at a time.
If recursion goes too deep without a base case, the stack exceeds its size limit and throws RangeError: Maximum call stack size exceeded (stack overflow).
The runtime environment (browser or Node.js) provides APIs that operate outside the main thread. When you call setTimeout, fetch, or add an event listener, the browser hands the operation to its own internal threads. When the operation completes (timer expires, response arrives, event fires), the callback is placed into the appropriate queue — not directly onto the call stack.
Holds callbacks from: setTimeout, setInterval, I/O operations, UI rendering events, and setImmediate (Node.js). The event loop processes one macrotask per cycle.
Holds callbacks from: Promise.then/.catch/.finally, queueMicrotask(), MutationObserver, and the internal mechanics of async/await. Microtasks have higher priority than macrotasks.
The event loop follows this cycle continuously:
The critical insight: all microtasks run before the next macrotask. If a microtask enqueues another microtask, that also runs before any macrotask. This means Promise.resolve().then(callback) always executes before setTimeout(callback, 0), regardless of code order.
requestAnimationFrame(callback) runs after microtasks but before the browser paints — it's synchronized with the display refresh rate (typically 60fps). It's neither a macrotask nor a microtask. Use it for visual animations to ensure smooth rendering.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');Output: 1, 4, 3, 2. Synchronous code runs first (1, 4), then the microtask (3), then the macrotask (2).
JavaScript itself is single-threaded, but the runtime environment is not. The event loop bridges synchronous execution with asynchronous APIs. Microtasks always have priority over macrotasks — this explains why promise callbacks run before timer callbacks. Understanding this model is essential for predicting async execution order and avoiding performance pitfalls like microtask starvation (infinite microtask loops that block rendering).
Fun Fact
Philip Roberts' JSConf EU 2014 talk 'What the heck is the event loop anyway?' has over 8 million views on YouTube and is widely considered the definitive explanation of the event loop. His visualization tool Loupe lets you step through code and watch the call stack, Web APIs, and callback queue in real time.