JavaScript is often called single-threaded.
That means it has one main thread and can execute one piece of JavaScript at a time.
So here is the obvious question:
If JavaScript can do only one thing at a time, how can it handle all of this?
- API requests
- Timers
- Button clicks
- User input
- File operations
- Animations
For example:
console.log("Start");
setTimeout(() => {
console.log("Timer finished");
}, 2000);
console.log("End");
The output is:
Start
End
Timer finished
But why?
If JavaScript waits for the timer to finish, shouldn't the output be:
Start
Timer finished
End
The answer lies in one of the most important concepts in JavaScript:
The Event Loop
To understand it properly, we need to understand the complete system:
JavaScript Runtime
│
├── Call Stack
│
├── Web APIs
│
├── Callback Queue
│
├── Microtask Queue
│
└── Event Loop
Let's break it down step by step.
1. The Call Stack — Where JavaScript Executes Code
The Call Stack is where JavaScript executes functions.
Think of it as a stack of tasks.
The last task added to the stack is the first one to be executed.
For example:
function first() {
second();
}
function second() {
console.log("Hello");
}
first();
The Call Stack roughly works like this:
Call Stack
first()
↓
second()
↓
console.log("Hello")
As each function finishes, it is removed from the stack.
first() added
↓
second() added
↓
console.log() runs
↓
second() removed
↓
first() removed
This is why JavaScript is synchronous by default.
It executes one operation at a time.
Task 1
↓
Task 2
↓
Task 3
The next task doesn't start until the current task is complete.
But what happens when JavaScript encounters something that takes time?
For example:
setTimeout(() => {
console.log("Done");
}, 2000);
JavaScript does not sit on the Call Stack for two seconds.
That would block everything else.
Instead, it hands the timer to the browser.
2. Web APIs — Where the Browser Handles Asynchronous Work
The browser provides powerful features called Web APIs.
These include:
setTimeoutfetch- DOM events
addEventListener- Geolocation
- And many other browser capabilities
Consider this:
console.log("Start");
setTimeout(() => {
console.log("Timer finished");
}, 2000);
console.log("End");
Here is what happens.
First:
console.log("Start")
is pushed onto the Call Stack and executed.
Output:
Start
Then JavaScript reaches:
setTimeout(callback, 2000);
The timer is registered with the browser's Web APIs.
Call Stack
│
│ setTimeout(...)
▼
Web APIs
│
│ Wait 2 seconds
▼
Callback is ready
The Call Stack is now free to continue executing the rest of the code.
So:
console.log("End");
runs immediately.
Output:
Start
End
After two seconds, the timer's callback is ready.
But it still cannot directly jump back onto the Call Stack.
It first has to wait in a queue.
3. Callback Queue — Where Regular Async Callbacks Wait
Once an asynchronous operation is complete, its callback can be placed into the Callback Queue, also called the Task Queue.
Web APIs
│
│ Async operation completes
▼
Callback Queue
│
│ Waiting...
▼
Call Stack
Let's look at the timer again:
setTimeout(() => {
console.log("Timer finished");
}, 2000);
After two seconds:
Callback Queue
[ console.log("Timer finished") ]
But the callback cannot execute yet.
It has to wait until the Call Stack is empty.
This is where the Event Loop comes in.
4. The Event Loop — The Traffic Controller
The Event Loop constantly checks:
Is the Call Stack empty?
If the answer is no, JavaScript keeps executing the current code.
If the answer is yes, the Event Loop can move waiting work into the Call Stack.
You can think of it like this:
Is Call Stack Empty?
│
┌────────┴────────┐
│ │
No Yes
│ │
▼ ▼
Keep executing Check queues
current code │
▼
Move next task
to the Call Stack
So the full timer flow becomes:
setTimeout(...)
↓
Web APIs
↓
Wait 2 seconds
↓
Callback Queue
↓
Event Loop checks Call Stack
↓
Call Stack is empty
↓
Callback executes
That is why the callback runs later.
5. But There Is Another Queue: Microtasks
This is where the Event Loop becomes more interesting.
JavaScript does not have only one queue.
There is also a Microtask Queue.
Microtasks usually include:
Promise.then()Promise.catch()Promise.finally()queueMicrotask()
For example:
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise");
});
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
What do you think the output will be?
Start
End
Promise
Timeout
Many developers expect setTimeout(..., 0) to run first.
But it doesn't.
Why?
Because Microtasks have higher priority than regular tasks.
The Event Loop checks the Microtask Queue before taking the next regular callback from the Callback Queue.
6. Microtasks Get Priority
Let's follow the previous example step by step.
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise");
});
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
Step 1: console.log("Start")
The Call Stack executes it.
Output:
Start
Step 2: Promise callback is registered
The Promise callback is placed in the Microtask Queue when ready.
Microtask Queue
[ console.log("Promise") ]
Step 3: setTimeout is registered
The browser handles the timer.
Once ready, its callback goes to the regular queue.
Callback Queue
[ console.log("Timeout") ]
Step 4: console.log("End")
The synchronous code continues.
Output:
Start
End
Now the Call Stack becomes empty.
The Event Loop checks the queues.
But it does not take the regular callback first.
It checks:
1. Microtask Queue
2. Callback Queue
So:
Promise
runs first.
Then:
Timeout
runs.
Final output:
Start
End
Promise
Timeout
7. The Complete Event Loop Flow
Here is the simplified mental model:
JavaScript Code
│
▼
Call Stack
│
┌────────────┼────────────┐
│ │
Synchronous Code Async Operation
│ │
▼ ▼
Execute Now Web APIs
│
┌──────────────┴──────────────┐
│ │
▼ ▼
Microtask Queue Callback Queue
Promise.then() setTimeout()
queueMicrotask() Events
Other tasks
│ │
└──────────────┬──────────────┘
▼
Event Loop
│
Is Call Stack Empty?
│
▼
Process Microtasks First
│
▼
Then Process Next Task
This is the key rule to remember:
Synchronous code → Microtasks → Regular tasks
8. A Real Example That Confuses Many Developers
Look at this:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
What will the output be?
1
4
3
2
Let's understand why.
First, synchronous code runs:
console.log("1")
console.log("4")
Output:
1
4
Next, the Call Stack becomes empty
The Event Loop checks the Microtask Queue first.
So:
3
runs.
Finally, the regular Callback Queue is processed.
So:
2
runs.
The final result:
1
4
3
2
This single example explains a huge part of how the Event Loop works.
9. Does setTimeout(..., 0) Run Immediately?
No.
This is a common misunderstanding.
setTimeout(() => {
console.log("Hello");
}, 0);
The 0 does not mean:
"Run this immediately."
It means:
"Run this callback after at least the specified delay, when the browser gets a chance to schedule it and the Call Stack is available."
So this:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
still produces:
Start
End
Timeout
Because the synchronous code must finish first.
The callback also has to wait for its turn in the Event Loop.
10. Why Can JavaScript Still Feel Like It Does Multiple Things at Once?
Because JavaScript doesn't necessarily perform every operation itself.
It can delegate work.
Think of JavaScript as a manager.
JavaScript
│
├── "Browser, handle this timer."
│
├── "Browser, wait for this click."
│
├── "Network, fetch this data."
│
└── Continues executing other code
While those operations happen outside the Call Stack, JavaScript can continue doing other work.
When the result is ready:
Async operation completes
↓
Callback enters a queue
↓
Event Loop waits for the right moment
↓
Callback enters the Call Stack
↓
JavaScript executes it
So JavaScript is still executing one thing at a time on its main thread.
The surrounding environment makes asynchronous behavior possible.
11. Why Does the Event Loop Matter in Real Applications?
The Event Loop is not just an interview concept.
It directly affects how applications behave.
For example, if you run a heavy task:
while (true) {
// Heavy work
}
The Call Stack never becomes available.
That means:
- Button clicks cannot be processed
- Timers cannot run
- Promise callbacks cannot execute
- The page can become unresponsive
The Event Loop is effectively blocked because JavaScript is still busy.
This is why long-running JavaScript tasks can freeze a webpage.
Understanding the Event Loop helps you understand:
- Why a UI becomes unresponsive
- Why timers don't always run exactly on time
- Why Promises often execute before
setTimeout - How asynchronous JavaScript actually works
- Why blocking the main thread is expensive
The Simplest Way to Remember Everything
Think of the system as a restaurant.
Call Stack
= The chef
Web APIs
= The kitchen equipment handling long tasks
Microtask Queue
= High-priority orders
Callback Queue
= Regular orders
Event Loop
= The manager checking what the chef should do next
The chef can only prepare one thing at a time.
While the chef is working, other tasks can be handled elsewhere.
When the chef becomes free:
1. High-priority orders are checked first
2. Then regular orders are handled
That is the Event Loop in a simple mental model.
Final Mental Model
Whenever you see asynchronous JavaScript, remember this:
JavaScript starts executing
↓
Synchronous code runs on the Call Stack
↓
Async work is delegated to Web APIs
↓
Completed callbacks wait in queues
↓
Call Stack becomes empty
↓
Event Loop checks Microtasks first
↓
Microtasks execute
↓
Then regular callbacks execute
Or even more simply:
Call Stack
↓
Web APIs
↓
Microtasks / Callback Queue
↓
Event Loop
↓
Call Stack
JavaScript may be single-threaded, but with the help of the browser, queues, and the Event Loop, it can handle a huge number of asynchronous operations without stopping your entire application.
That is what makes modern JavaScript applications feel fast and responsive.
Top comments (0)