DEV Community

BysonTech
BysonTech

Posted on

How JavaScript Async Actually Works (Event Loop, Micro tasks, and Call Stack)

If you have ever thought:

  • Why does Promise.then() run before setTimeout?
  • Why does await run “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")
Enter fullscreen mode Exit fullscreen mode

Output:

A  
B  
C  
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Execution order:

1 main()  
2 a()  
3 b()  
4 console.log()  
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Output:

start  
end  
timeout  
Enter fullscreen mode Exit fullscreen mode

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:

  1. setTimeout is called
  2. It is handed off to Web APIs
  3. Timer starts outside the call stack
  4. 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:

  1. Run everything in the call stack
  2. When empty → take one task from the task queue
  3. Push it to the call stack
  4. Repeat

7. Full Async Flow

Example:

console.log("A")

setTimeout(() => {
  console.log("B")
}, 0)

console.log("C")
Enter fullscreen mode Exit fullscreen mode

Execution:

A  
C  
B  
Enter fullscreen mode Exit fullscreen mode

Why:

  1. A runs
  2. setTimeout registers callback
  3. C runs
  4. call stack becomes empty
  5. event loop pushes task
  6. 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")
Enter fullscreen mode Exit fullscreen mode

Output:

start  
end  
promise  
timeout  
Enter fullscreen mode Exit fullscreen mode

Because microtasks run first.


11. Event Loop Order (Important)

The real execution order:

  1. Run call stack
  2. Run ALL microtasks
  3. Run ONE task
  4. Run microtasks again
  5. Repeat

This is the core rule.


12. What async Really Does

async function test() {
  return 1
}
Enter fullscreen mode Exit fullscreen mode

This is actually:

Promise.resolve(1)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Output:

A  
C  
B  
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

is conceptually:

getData().then(value => {
  console.log(value)
})
Enter fullscreen mode Exit fullscreen mode

15. await Splits Execution

Example:

async function main() {

  console.log("1")

  await Promise.resolve()

  console.log("2")

}

console.log("3")

main()

console.log("4")
Enter fullscreen mode Exit fullscreen mode

Output:

3  
1  
4  
2  
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Output:

A  
B  
D  
C  
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Parallel:

const [a, b] = await Promise.all([
  fetchA(),
  fetchB()
])
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

then:

  1. fetchA runs
  2. wait
  3. fetchB runs

But if you write:

const aPromise = fetchA()
const bPromise = fetchB()

const a = await aPromise
const b = await bPromise
Enter fullscreen mode Exit fullscreen mode

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)