Back to Blog
Javascript

JS Event Loop

๐Ÿš€ JavaScript may be single-threaded, but it never feels slow. The secret? The

Wajahat Zia
Wajahat Zia
9/9/2025
0 views

JavaScript runs on a single thread, which means it can only do one thing at a time.

So how does it still handle things like timers, network requests, and promises without getting โ€œstuckโ€?

๐Ÿ‘‰ Thatโ€™s where the event loop comes in.

Itโ€™s the system that makes JavaScript look asynchronous even though it runs in one thread.

Letโ€™s break it down.

Key Parts of the Event Loop

Call Stack โ†’ Runs all your synchronous code, line by line.
Task Queue (Macrotask Queue) โ†’ Stores callbacks from things like setTimeout, setInterval, fetch, and DOM events.
Microtask Queue โ†’ Stores callbacks from promises (then, catch, finally), queueMicrotask(), and async/await. This queue is always checked before macrotasks.
Web APIs โ†’ Browser features like timers, network requests, or event listeners that schedule tasks into the queues.

The Rules of the Event Loop

1.
Run everything in the Call Stack first (synchronous code).
2.
When the stack is empty, check the Microtask Queue and run all pending microtasks.
3.
If no microtasks remain, take the next callback from the Task Queue and run it.
4.
Repeat forever.

So, microtasks always โ€œcut in lineโ€ before macrotasks.

Examples

Case 1

javascript
function foo() {
console.log(1)
}
console.log(2)
foo()
javascript
// Expected Output
// 2
// 1

Lets see how the Event Loop handles

1.
console.log(2) is pushed onto the Call Stack and executed. We see the output 2
2.
foo() is pushed onto the Call Stack and executed.
3.
console.log(1) is pushed onto the Call Stack and executed. We see the output 1
4.
No microtask or macrotask were present in the code

Case 2

javascript
function foo() {
console.log(1)
}
console.log(2)
setTimeout(() => {
console.log(3) // schedule a macrotask
}, 0)
foo()
javascript
// Expected Output
// 2
// 1
// 3
1.
console.log(2) pushed on the Call Stack and executed. Output 2
2.
setTimeout is pushed onto the Call Stack and executed. Its callback function is pushed onto the Task Queue
3.
foo() is pushed onto the Call Stack and executed.
4.
console.log(1) is pushed onto the Call Stack and executed. We see the output 1
5.
All synchronous code has executed. The Event Loop checks the Microtasks Queue. It is empty so moves onto the Task Queue
6.
Task Queue is inspected. It contains 1 callback pending from our Step 2. The callback is pushed onto the Call Stack and executed. We see the output 3

Case 3

javascript
function foo() {
console.log(1)
}
console.log(2)
setTimeout(() => {
console.log(3) // schedule a macrotask
}, 0)
Promise.resolve().then(() => {
console.log(4) // schedule a microtask
})
foo()
javascript
// Expected Output
// 2
// 1
// 4
// 3
1.
console.log(2) pushed on the Call Stack and executed. Output 2
2.
setTimeout is pushed onto the Call Stack and executed. Its callback function is pushed onto the Task Queue
3.
Promise is pushed onto the Call Stack and executed. The callback in then is pushed onto the Microtask Queue
4.
foo() is pushed onto the Call Stack and executed.
5.
console.log(1) is pushed onto the Call Stack and executed. We see the output 1
6.
All synchronous code has executed. The Event Loop checks the Microtasks Queue.
7.
The Microtask Queue contains a pending callback from Step 3. Microtask Queue has higher priority than Task Queue so it executes the pending callbacks. We see the output 4
8.
No pending Microtasks, Event Loop moves onto the Task Queue
9.
Task Queue is inspected. It contains 1 callback pending from our Step 2. The callback is pushed onto the Call Stack and executed. We see the output 3

Case 4

javascript
setTimeout(() => {
console.log("timeout 1");
// Schedule a microtask inside the macrotask
Promise.resolve().then(() => {
console.log("microtask 1");
// Schedule ANOTHER macrotask
setTimeout(() => {
console.log("timeout 2");
}, 0);
});
}, 0);
setTimeout(() => {
console.log("timeout 3");
}, 0);
console.log(1)
javascript
// Expected Output
// 1
// "timeout 1"
// "microtask 1"
// "timeout 3"
// "timeout 2"
1.
setTimeout is pushed onto the Call Stack and executed. Its callback is added to the Task Queue
2.
Second setTimeout is pushed onto the Call Stack and executed. Its callback is added to the Task Queue
3.
console.log(1) is pushed onto the Call Stack and executed. Output 1
4.
Microtask Queue is currently empty, Event Loop scans Task Queue
5.
Task Queue contains two pending callbacks, 1st callback is executed.
6.
Before executing the second callback, our Event Loop sees there is a pending microtask. As Microtask Queue takes priority, we notice
7.
Now the Macrotask Queue is drained, we can move back to the Task Queue
8.
The Task Queue contains two callback
9.
The Event Loop begins to execute and drain the Task Queue

Case 5

Any code within the then, catch and finally or any code after await in the code block is scheduled as a microtask

javascript
console.log("start");
Promise.resolve()
.then(() => {
console.log("then 1");
})
.then(() => {
console.log("then 2");
});
console.log("end");
javascript
// Expected Output
// start
// end
// then 1
// then 2
console.log("start") โ†’ start
Promise.resolve() is already resolved, so .then(...) callbacks go into the microtask queue.
console.log("end") โ†’ end
Event loop finishes sync code โ†’ process microtasks:
javascript
async function run() {
console.log("start");
await Promise.resolve();
console.log("then 1");
await Promise.resolve();
console.log("then 2");
}
run();
console.log("end");
javascript
// Expected Output
// start
// end
// then 1
// then 2
Call run() โ†’ inside async function:
console.log("end") โ†’ end
Event loop processes microtasks:

Summary

The JavaScript Event Loop is a mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded. Here's what we've learned:

Call Stack: Executes synchronous code in a Last-In-First-Out (LIFO) manner.
Task Queue (Macrotask Queue): Holds callbacks from asynchronous operations like setTimeout, setInterval, and I/O operations. These are processed after the Call Stack is empty.
Microtask Queue: Contains callbacks from Promises and async/await operations. These have higher priority than the Task Queue and are processed immediately after the Call Stack empties before any macrotasks.
Execution Order:
Key Patterns:

Understanding the Event Loop is crucial for predicting code execution order in JavaScript and avoiding common asynchronous programming pitfalls.

JavascriptFundamentals