The Savage Truth About the JavaScript Event Loop.

Many times, we think about how JavaScript actually works. We come across terms like V8 engine, single-threaded, callbacks, asynchronous, non-blocking, Event loop and concurrency— all these fancy terms — but we hardly try to understand how they actually work.
Let’s look at the diagram below — This represents the JavaScript runtime and how it looks:

The image above actually represents the JavaScript runtime — or, in Chrome, what we call the V8 engine — where your JavaScript code executes. If you clone the V8 engine’s source code and try to grep for setTimeout
, you won’t find it.
Wait, what?
Isn’t setTimeout
something we use a lot in JavaScript? Yet, it’s not even present in the JavaScript runtime itself?
In the JavaScript runtime environment provided by the browser, we have access to Web APIs, which handle functionalities like setTimeout
, DOM manipulation, and network requests — these are not part of the core JavaScript language itself, but are provided by the browser."
Note: In Node.js, where there is no browser, we have the libuv layer — this is where setTimeout
resides.
Let’s enhance our diagram now, from our learning above:

Now, let's see how our code gets executed in JavaScript:

Here’s how the program works: as soon as you run the program, let's say we call it, main()
, it is pushed to the Call Stack. This then calls printAddNumbers()
, which is also pushed onto the Call Stack. That function further calls addTwo()
, which is similarly pushed onto the stack. This continues as each function calls another.
Note: None of the functions are popped from the Call Stack yet — the reason is that they haven’t completed execution. Simple...
Ok, now lastly addTwo()
will call the last function, add()
, which is then pushed onto the stack. As soon as we hit the return statement num + increment
, add()
is popped from the stack. Then it returns to addTwo()
, which also gets popped after its return is executed. Control then goes back to printAddNumbers()
, which prints the sum and gets popped from the stack. Now, again our Call Stack is empty and ready to take another code. This is how our code generally executes.
We should draw some key points here:
- Notice that JavaScript executes one function at a time — this is what “single-threaded” actually means.
- As soon as the return statement is hit, the function gets popped from the stack.
Lets take another example we can take is:

Notice that we are calling foo()
, which gets pushed onto the Call Stack and then calls foo()
again, which will push to the Call Stack again, and there is no coming back. This infinite loop scenario is causing what is called StackOverflow. Hope it make sense to you now...
So far, we’ve covered the JavaScript runtime — specifically the V8 engine in Chrome — and the concept of it being single-threaded.
Let's hop on to Blocking and Non-blocking stuff.
Let's analyse an assumption below. Note: it's just an assumption.
Suppose we’re making an API call. This response can take anywhere from a few milliseconds to several seconds, depending on various factors.

Since JavaScript is single-threaded, as we know, we might assume that these API calls are executed one by one in the Call Stack.
Let’s assume that URL2
takes some time to return data, which would block the URL3
API call. Also, during this time, we wouldn’t be able to click buttons or perform any other operations in the browser, because our single thread/call stack is busy handling the network request.
So we can clearly understand Something's Wrong. In real life, this never happens.
So, that didn’t work. Now, let’s see how asynchronous callbacks come into the picture and help us solve the issue above.
See this example below:

You might have seen these examples or even been asked about them multiple times in interviews. They suggest that there’s a component we’re missing. This behaviour is asynchronous, yet so far we haven’t seen anything that clearly shows how JavaScript handles asynchrony. Let’s dive into that now.
There are two more components missing from our original diagram. Let’s complete the diagram:

If you look at the diagram, you’ll notice two more components — the Legendary Event Loop and the Callback Queue.
Let's pick our setTimeout code and try to fit this above diagram.
Step 1:

- When we run our program,
console.log(1)
goes directly to the Call Stack, gets executed, and prints1
to the console. setTimeout
goes to the Web APIs — notice that it does not go directly to the Call Stack. Instead, a timer starts running for the duration you’ve specified.
Step 2

- Since the timer is set to 0 ms, it completes immediately, and the callback
() => console.log(2)
is pushed to the Callback Queue - Since the Call Stack is empty,
console.log(3)
gets pushed onto it and is executed.
Step 3

- Now that
3
has been executed, you can see it in the console output. - But how does
console.log(2)
get executed? This is where the Event Loop comes into the picture. - The Event Loop continuously checks whether the Call Stack is empty. If it is, the Event Loop takes the first element from the Callback Queue and pushes it onto the Call Stack.
Most important point: If your Call Stack is busy — for example, you’re running an infinite loop — then the event loop can’t push the setTimeout
callback to the stack. As a result, the setTimeout
callback never gets executed, even if the timer has expired.
This leads us to an important conclusion:
setTimeout
guarantees a minimum delay, not an exact delay, which you might have assumed.
If the Call Stack is engaged at that moment, the actual execution of the callback will be delayed further — or potentially blocked forever.
This is how the event loop works and how asynchronous operations happen in JavaScript.
Now that we’ve understood single-threaded behaviour, asynchronous operations, non-blocking execution, and the event loop, there’s one thing that was either implemented incorrectly above, or I was saving it for the end.
"The thing is — where do Promises come into the picture, and how do they get handled? Promises essentially work like callbacks. In fact, some asynchronous operations return callbacks, while others return Promises. Both serve the same goal — handling async behaviour — but Promises provide a cleaner, more structured way to chain and manage asynchronous operations.
Before jumping into this, I’m assuming you already have a basic understanding of Promises, .then
, and related concepts.
Let's solve this via an example.

Look at the output of the above image:
- We have
console.log(3),
which immediately gets executed since the Call Stack was empty. - After that, Promise immediately gets resolved and goes to the Web APIs; further, the callback is pushed to the callback queue and executed.
- Same with setTimeout, goes to Web APIs bla bla bla...
"Wait a minute, what is the explanation for the Promise getting resolved first and setTimeout
second? Rest assured, it's not because setTimeout
is written second. You can try it if you want!"
There's one thing we haven't explored in depth yet—let's dive into it: the Callback Queue.
Actually, the diagram we were covering is incomplete. The Callback Queue isn’t alone—there are two parts: the MacroTask Queue and the Microtask Queue.
Lets redraw our diagram:

Important points to notice are:
- "Web APIs place
setTimeout
and other timers into the Macro Task Queue, whilefetch
and Promises go into the Microtask Queue. All other mechanisms remain the same.
Let's solve our example based on the above new diagram:

Step 1:
console.log(3)
goes to the Call Stack since it's empty, and gets executed immediately, printing3
to the console. Nothing rocket science here.- As we discussed,
setTimeouts
are handled by the Web APIs. Promises are queued in the microtask queue managed by the JavaScript engine itself. Once the timer completes or the Promise resolves, they are pushed to their respective queues.

Step 2:
- Callback from promise is pushed to MicroTask Queue, and callback from SetTimeout is pushed to macro task queue.
- Even Loop work is to keep on checking the Call Stack to push things to it.
Wait, which one goes first: the Microtask Queue or the Macro Task Queue?
Most important Note: The Microtask Queue always has higher priority than the Macro Task Queue. If the Microtask Queue continues to flood the Call Stack, Macrotask callbacks may get starved and never execute.
Now it makes sense why 1
was executed first, and then we saw 2
from setTimeout
.
I hope this has cleared up some of your long-held confusion and concerns.
Hit subscribe for more posts like this.
Cheers!