JavaScript is often described as a single-threaded language.
At a high level, this means JavaScript executes JavaScript code on a single main thread, handling one piece of JavaScript execution at a time.
But modern applications constantly perform operations that take time:
- API requests
- Timers
- Button clicks
- User input
- File operations
- Animations
So an important question comes up:
If JavaScript executes one piece of code at a time, how can it handle asynchronous operations without freezing the application?
The answer is the Event Loop.
To understand it, we need to look at the different pieces involved:
JavaScript Runtime
Call Stack
│
▼
JavaScript executes
│
┌───────────┴───────────┐
│ │
Synchronous Async work
code │
▼
Host APIs
│
┌──────────┴──────────┐
▼ ▼
Microtask Queue Task Queue
│ │
└──────────┬──────────┘
▼
Event Loop
│
▼
Call Stack
Let's understand how this works.
1. The Call Stack
The Call Stack is where JavaScript keeps track of the functions it is currently executing.
Consider:
function first() {
second();
}
function second() {
console.log("Hello");
}
first();
When first() is called, it goes onto the Call Stack.
Then first() calls second(), so second() is added on top.
Conceptually:
Call Stack
┌─────────────────┐
│ second() │
├─────────────────┤
│ first() │
└─────────────────┘
second() finishes first, so it is removed.
Then first() finishes.
This follows the LIFO principle:
Last In, First Out.
The important thing is that JavaScript cannot execute two pieces of JavaScript code simultaneously on the same main execution thread.
So if one task keeps the Call Stack busy for a long time, other JavaScript work has to wait.
2. What Happens With Asynchronous Work?
Now consider:
console.log("Start");
setTimeout(() => {
console.log("Timer finished");
}, 2000);
console.log("End");
You might initially expect:
Start
Timer finished
End
But the actual output is:
Start
End
Timer finished
Why doesn't JavaScript wait for two seconds?
Because the timer does not remain on the Call Stack.
The surrounding environment, such as the browser, provides APIs that can handle operations like timers and network requests.
A simplified flow looks like this:
Call Stack
│
│ setTimeout(...)
▼
Host / Browser APIs
│
│ wait for timer
▼
Timer becomes ready
│
▼
Task Queue
Meanwhile, JavaScript is free to continue executing:
console.log("End");
That's why "End" appears before "Timer finished".
3. Host APIs Handle Asynchronous Operations
The browser provides many capabilities outside the JavaScript Call Stack.
Examples include:
- Timers
- DOM events
- Network requests
- Geolocation
- Other browser functionality
For example:
setTimeout(() => {
console.log("Done");
}, 2000);
The timer is registered with the host environment.
JavaScript doesn't sit there doing nothing for two seconds.
Instead:
JavaScript
│
│ Register timer
▼
Browser / Host
│
│ Timer runs
▼
Callback becomes ready
│
▼
Task Queue
This is one of the key ideas behind asynchronous JavaScript:
JavaScript can delegate certain operations to its surrounding environment and continue executing other JavaScript code.
4. The Task Queue
When an asynchronous operation becomes ready, its callback may be scheduled as a task.
For example:
setTimeout(() => {
console.log("Timeout");
}, 0);
Once the timer is eligible to fire, its callback is placed into the appropriate task queue.
Conceptually:
Task Queue
┌──────────────────────────┐
│ console.log("Timeout") │
└──────────────────────────┘
But being in the queue doesn't mean the callback executes immediately.
The Call Stack must be available, and the runtime's scheduling rules determine when the task gets a chance to run.
That's where the Event Loop comes in.
5. The Event Loop
The Event Loop coordinates when queued work gets an opportunity to execute.
A simplified mental model is:
Call Stack
│
▼
Is it empty?
│
┌────┴────┐
│ │
No Yes
│ │
▼ ▼
Continue Process
execution queued work
When JavaScript is busy executing code, queued callbacks have to wait.
When the current execution finishes, the runtime can process pending asynchronous work according to its scheduling rules.
This continuous coordination is what we call the Event Loop.
6. Microtasks: The Important Second Queue
Things become more interesting when Promises are involved.
Consider:
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise");
});
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
The output is:
Start
End
Promise
Timeout
Why?
Because Promise reactions are scheduled as microtasks.
Examples of microtasks include:
Promise.then()Promise.catch()Promise.finally()queueMicrotask()
So we can simplify the model into two categories:
Microtask Queue
│
├── Promise callbacks
└── queueMicrotask()
Task Queue
│
├── Timers
├── User interaction tasks
└── Other scheduled tasks
7. Why Does the Promise Run Before setTimeout?
Let's break down the previous example.
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise");
});
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
Step 1: Synchronous code executes
First:
console.log("Start");
Output:
Start
Then the Promise reaction is scheduled as a microtask.
The timer is also registered.
Finally:
console.log("End");
runs.
Output so far:
Start
End
Now the current JavaScript execution has finished.
Step 2: Microtasks are processed
The Promise callback is waiting in the Microtask Queue:
Microtask Queue
[ Promise callback ]
The runtime processes pending microtasks before moving on to the next task.
So:
Promise
is printed.
Step 3: The timer task gets its turn
The timer callback is waiting as a task.
So:
Timeout
is printed.
Final output:
Start
End
Promise
Timeout
The key idea is:
After the current JavaScript execution finishes, pending microtasks are processed before the next task gets a chance to run.
8. A Classic Event Loop Question
Consider:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
What will be printed?
1
4
3
2
Let's see why.
Synchronous code
console.log("1");
console.log("4");
produces:
1
4
Then the current execution finishes.
The Promise callback is a microtask:
Microtask Queue
↓
3
The timer callback is a task:
Task Queue
↓
2
Microtasks are processed first:
3
Then the timer task runs:
2
Therefore:
1
4
3
2
9. Does setTimeout(..., 0) Mean "Run Immediately"?
No.
This is one of the most common misconceptions about JavaScript timers.
Consider:
setTimeout(() => {
console.log("Hello");
}, 0);
The 0 does not mean:
Run this callback immediately.
It means the timer has a minimum delay of approximately zero milliseconds before the callback becomes eligible to be scheduled.
It still has to wait for:
Current JavaScript execution
↓
Microtasks
↓
Scheduling opportunity
↓
Timer callback
For example:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
produces:
Start
End
Timeout
Even with a zero-millisecond delay.
10. What If JavaScript Is Busy?
Here's where the Event Loop becomes especially important.
Consider:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
for (let i = 0; i < 10000000000; i++) {
// Heavy computation
}
console.log("End");
The timer may become ready while the loop is running.
But its callback cannot interrupt the currently executing JavaScript.
The Call Stack is still busy.
Conceptually:
Call Stack
│
▼
Heavy computation
│
│
│ Timer becomes ready
│
▼
Task Queue
│
│ WAIT
│
▼
Call Stack becomes available
│
▼
Timer callback executes
This is why a long-running JavaScript task can make a web page feel frozen.
Buttons may stop responding.
Animations can appear stuck.
Input can feel delayed.
Timers may execute later than expected.
11. JavaScript Isn't Doing Everything by Itself
A useful way to understand asynchronous JavaScript is to think of JavaScript as one worker coordinating with its environment.
For example:
JavaScript
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
Timer Network Events
│ │ │
└──────────┼──────────┘
▼
Queues
│
▼
Event Loop
│
▼
Call Stack
JavaScript doesn't need to sit on the Call Stack waiting for every external operation to finish.
The host environment can handle the operation and later schedule the appropriate work for JavaScript.
This is what allows applications to remain responsive while waiting for things such as network responses or user interactions.
12. Why the Event Loop Matters
The Event Loop isn't just an interview question.
It directly affects real applications.
UI responsiveness
If the main thread is blocked by expensive JavaScript, the browser has less opportunity to process user interactions and update the page.
API calls
Network operations can take unpredictable amounts of time. JavaScript can continue executing other work instead of blocking the entire application while waiting.
Timers
A timer specifies when its callback becomes eligible, not an exact guarantee that it will execute at that exact moment.
Promises
Understanding microtasks explains why Promise callbacks often run before timer callbacks.
Performance
Large synchronous tasks can delay everything waiting for the main thread.
Understanding the Event Loop helps developers reason about all of these behaviors.
13. The Simplified Mental Model
You don't need to memorize every implementation detail.
Start with this model:
JavaScript
│
▼
Call Stack
│
┌─────────┴─────────┐
│ │
Sync execution Async operation
│
▼
Host APIs
│
┌────────┴────────┐
▼ ▼
Microtask Queue Task Queue
│ │
└────────┬────────┘
▼
Event Loop
│
▼
Call Stack
And remember the basic ordering:
Current JavaScript
↓
Microtasks
↓
Next task
That's the core idea.
14. The Restaurant Analogy
Here's another simple way to remember it.
Imagine a restaurant.
Call Stack
= Chef
Host APIs
= Kitchen staff handling things in the background
Microtask Queue
= High-priority orders
Task Queue
= Regular orders
Event Loop
= Manager deciding what the chef should handle next
The chef can prepare only one order at a time.
While something else is being prepared by the kitchen staff, the chef can work on another order.
When the chef becomes available, the manager coordinates which waiting work gets processed next.
The analogy isn't perfect, but it gives you the right intuition:
One execution thread doesn't mean the entire application has to stop whenever something takes time.
Final Mental Model
When you see asynchronous JavaScript, think:
1. JavaScript starts executing
↓
2. Synchronous code runs on the Call Stack
↓
3. Some operations are handled by the host environment
↓
4. Completed work is scheduled
↓
5. Current JavaScript execution finishes
↓
6. Microtasks are processed
↓
7. A task gets an opportunity to execute
↓
8. The cycle continues
Or, in one line:
Call Stack
↓
Host APIs
↓
Microtasks / Tasks
↓
Event Loop
↓
Call Stack
The most important thing to remember is that JavaScript's single-threaded execution model doesn't mean the application can perform only one kind of work at a time.
The JavaScript engine, host environment, queues, and Event Loop work together to coordinate asynchronous operations.
Once you understand that model, concepts like Promises, timers, API requests, event handlers, and UI responsiveness become much easier to reason about.
The Event Loop is essentially the mechanism that keeps asynchronous JavaScript moving without blocking every piece of work behind it.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.