Understanding the Event Loop: V8 Engine Demystified
Back to Blog
JavaScript

Understanding the Event Loop: V8 Engine Demystified

Lalit Tomer
Dec 14, 2023
10 min read

Take a peek under the hood of the V8 JavaScript engine. Learn exactly how the call stack, web APIs, and the callback queue handle asynchronous code.

The Single-Threaded Illusion

JavaScript is famously described as a single-threaded, non-blocking, asynchronous, concurrent language. To a beginner, this sentence sounds like an oxymoron. How can a language be single-threaded (able to execute only one operation at a time) while simultaneously being non-blocking and concurrent (handling network requests, timeouts, and UI interactions seemingly all at once)?

The answer lies in the architecture of the JavaScript runtime environment (like a web browser or Node.js), and specifically, the Event Loop. At its core, the V8 engine has a single Call Stack. When a function executes, it is pushed onto the stack; when it returns, it is popped off. If you execute a massive `while` loop that takes 5 seconds to resolve, the stack is blocked for 5 seconds. In a browser, this means the UI completely freezes—buttons cannot be clicked, animations halt, and scrolling stops.

To prevent this, JS relies on Web APIs (provided by the browser) or C++ APIs (provided by Node.js). When you execute a `setTimeout` or an HTTP `fetch` request, the V8 engine does not block the stack waiting for the operation to complete. Instead, it hands the operation off to the browser's Web APIs. The browser handles the timer or the network request on a separate thread, entirely outside of the JavaScript engine, allowing the V8 call stack to immediately move on to the next line of code.

The Single-Threaded Illusion

Macrotasks, Microtasks, and the Queue

Once the external Web API finishes its task (e.g., the timer expires or the HTTP response arrives), it doesn't just jam the callback function back onto the call stack indiscriminately. That would disrupt currently executing code. Instead, it pushes the callback into a Queue.

The Event Loop is a continuously running process with a very simple job: it constantly checks if the Call Stack is empty. If the stack is empty, it looks at the Queue, takes the first callback, and pushes it onto the Call Stack for execution. However, it gets more complex because there isn't just one queue.

JavaScript differentiates between Macrotasks (like `setTimeout`, `setInterval`, UI rendering) and Microtasks (like `Promises` and `MutationObserver`). The Event Loop strictly prioritizes the Microtask queue. After the call stack empties, the Event Loop will execute *every single* callback in the Microtask queue before it touches a single callback in the Macrotask queue. This subtle architectural detail is the source of endless confusion for developers. If you have an infinite loop of Promises resolving each other, the Macrotask queue will starve, meaning your `setTimeout` callbacks will never run, and your UI will never re-render. Understanding this hierarchy is the key to writing highly performant, non-blocking JavaScript applications.

Macrotasks, Microtasks, and the Queue

Enjoyed this article?

Share it with your network.