If you have ever thought:
- Why does
Promise.then()run beforesetTimeout? - Why does
awaitrun “later” even though it looks synchronous? - What is actually happening behind async / await?
then you are ready to understand how JavaScript async really works.
When I first struggled with “why a Promise is returned”, I realized that I could not go further without understanding the internal mechanism.
In JavaScript, async behavior is built on these five concepts:
- Call Stack
- Web APIs
- Task Queue
- Microtask Queue
- Event Loop
Promise and async / await are just built on top of this system.
Let’s break it down step by step.
1. JavaScript Is Single-Threaded
JavaScript can execute only one thing at a time.
Example:
console.log("A")
console.log("B")
console.log("C")
Output:
A
B
C
This order is controlled by the Call Stack.
2. What Is the Call Stack?
The call stack is:
a stack that keeps track of currently executing functions
It follows LIFO (Last In, First Out).
Example:
function main() {
a()
}
function a() {
b()
}
function b() {
console.log("hello")
}
main()
Execution order:
1 main()
2 a()
3 b()
4 console.log()
The stack grows as functions are called and shrinks as they finish.
3. The Problem: Long-Running Tasks
Now consider this:
console.log("start")
setTimeout(() => {
console.log("timeout")
}, 0)
console.log("end")
Output:
start
end
timeout
Why does setTimeout run later, even with 0ms?
This is where Web APIs come in.
4. Web APIs
JavaScript engines (like V8) do not handle timers or network requests.
Features like:
- setTimeout
- fetch
are provided by the browser (or runtime environment).
Flow:
-
setTimeoutis called - It is handed off to Web APIs
- Timer starts outside the call stack
- JavaScript continues execution
5. Task Queue
When the timer finishes, the callback is not executed immediately.
It is placed into the:
Task Queue (Macrotask Queue)
This is a queue of functions waiting to be executed.
6. Event Loop
The event loop is:
a mechanism that checks if the call stack is empty
Flow:
- Run everything in the call stack
- When empty → take one task from the task queue
- Push it to the call stack
- Repeat
7. Full Async Flow
Example:
console.log("A")
setTimeout(() => {
console.log("B")
}, 0)
console.log("C")
Execution:
A
C
B
Why:
- A runs
- setTimeout registers callback
- C runs
- call stack becomes empty
- event loop pushes task
- B runs
8. Promise Is Special
Here is the important part:
Promise.then() does NOT use the normal task queue.
It uses a different queue called:
Microtask Queue
9. Microtasks vs Tasks
JavaScript has two queues:
Task Queue (Macrotasks)
- setTimeout
- setInterval
- DOM events
Microtask Queue
- Promise.then
- catch
- finally
- queueMicrotask
Key rule:
Microtasks run before tasks
10. Example
console.log("start")
setTimeout(() => {
console.log("timeout")
}, 0)
Promise.resolve().then(() => {
console.log("promise")
})
console.log("end")
Output:
start
end
promise
timeout
Because microtasks run first.
11. Event Loop Order (Important)
The real execution order:
- Run call stack
- Run ALL microtasks
- Run ONE task
- Run microtasks again
- Repeat
This is the core rule.
12. What async Really Does
async function test() {
return 1
}
This is actually:
Promise.resolve(1)
So:
async = function that returns a Promise
13. What await Really Does
Example:
async function main() {
console.log("A")
const value = await Promise.resolve(1)
console.log("B")
}
main()
console.log("C")
Output:
A
C
B
Why?
Because:
await splits the function into two parts using Promise.then
14. Mental Model of await
This:
async function main() {
const value = await getData()
console.log(value)
}
is conceptually:
getData().then(value => {
console.log(value)
})
15. await Splits Execution
Example:
async function main() {
console.log("1")
await Promise.resolve()
console.log("2")
}
console.log("3")
main()
console.log("4")
Output:
3
1
4
2
Everything after await becomes a microtask.
16. Another Example
console.log("A")
async function test() {
console.log("B")
await Promise.resolve()
console.log("C")
}
test()
console.log("D")
Output:
A
B
D
C
17. Key Insight About await
await does NOT block the thread
It just:
splits the function and schedules the rest as a microtask
18. Sequential vs Parallel
Sequential:
const a = await fetchA()
const b = await fetchB()
Parallel:
const [a, b] = await Promise.all([
fetchA(),
fetchB()
])
Important:
Parallelism depends on when the Promise starts, not when you await it
19. Why This Matters
If you write:
const a = await fetchA()
const b = await fetchB()
then:
- fetchA runs
- wait
- fetchB runs
But if you write:
const aPromise = fetchA()
const bPromise = fetchB()
const a = await aPromise
const b = await bPromise
both start immediately.
20. Big Picture
JavaScript async is built on:
- Call Stack → current execution
- Web APIs → timers, network
- Task Queue → setTimeout
- Microtask Queue → Promise
- Event Loop → orchestrates everything
21. Final Summary
The most important ideas:
- Promise → runs in microtask queue
- setTimeout → runs in task queue
- microtasks always run first
- await → splits code into microtasks
Once you understand this, you can explain:
- Why Promise runs before setTimeout
- Why await runs “later”
- Why sequential vs parallel happens
And at that point:
async / await stops being magic and becomes predictable.
Top comments (0)