JavaScript is single-threaded, but it can still handle asynchronous operations like timers, API calls, and promises efficiently. This often confuses developers — especially when setTimeout, Promise, and async/await don’t execute in the order we expect.
In this article, we will break down how JavaScript actually works behind the scenes using the Call Stack, Web APIs, Callback Queue, Microtask Queue, and the Event Loop.
We will start with a simple synchronous example, then gradually move into asynchronous behavior using setTimeout, Promise, and async/await. By the end, you will clearly understand why some code runs immediately while other parts are delayed — and how JavaScript manages everything without blocking the main thread.
If you've ever wondered:
- Why does
setTimeoutrun last? - Why does
Promise.then()run beforesetTimeout? - Does
async/awaitmake code synchronous?
Then this guide is for you
Example 1: Synchronous vs setTimeout
function a (){
b()
console.log("first")
}
function b (){
d()
console.log("second")
}
setTimeout(()=>{
console.log("async task1")
},2000)
setTimeout(()=>{
console.log("async task2")
},1000)
function d (){
e()
console.log("fourth")
}
function e (){
console.log("fifth")
}
a()
How It Works
JavaScript is single-threaded, meaning it uses one Call Stack to execute code.
🔹 Step 1: Synchronous Execution
Functions run one by one:
a() → b() → d() → e()
Output:
fifth
fourth
second
first
🔹 Step 2: setTimeout Behavior
-
setTimeoutis handled by Web APIs - After the timer finishes, it goes to the Callback Queue
- The Event Loop moves it to the Call Stack when it's empty
Final Output:
fifth
fourth
second
first
async task2
async task1
Event Loop (Simple Idea)
The Event Loop continuously checks:
“Is the Call Stack empty?”
If yes → it pushes tasks from the queue into the stack.
Example 2: Promise (Microtask Queue)
console.log("start")
setTimeout(() => {
console.log("timeout")
}, 0)
Promise.resolve().then(() => {
console.log("promise")
})
console.log("end")
Output:
start
end
promise
timeout
Why This Happens?
Because JavaScript has two queues:
🟡 Microtask Queue
- Promise
.then - async/await
🔵 Callback Queue (Macrotask)
- setTimeout
- setInterval
👉 Rule:
Microtasks always run before Callback Queue tasks.
Example 3: async/await
async function example() {
console.log("start")
await new Promise(resolve => setTimeout(resolve, 1000))
console.log("end")
}
console.log("before")
example()
console.log("after")
Output:
before
start
after
end
Important Concept
Many people think:
async/await makes code synchronous
❌ That’s incorrect
✔️ Correct idea:
async/await makes asynchronous code LOOK synchronous, but it still uses the Event Loop.
Key Takeaways
- JavaScript runs synchronous code first
- Async tasks go to Web APIs
- Event Loop manages execution
- Promises use Microtask Queue
- Microtasks run before setTimeout
- async/await is just syntax over Promise
Final Thought
JavaScript never pauses the entire program for async code.
It simply schedules it smartly using:
- Call Stack
- Event Loop
- Queues
Once you understand this, async JavaScript becomes much easier
If this helped you, feel free to share
Top comments (0)