JavaScript Event Loop Explained: Call Stack, Web APIs, Queues, Promises and setTimeout
If you've ever looked at JavaScript code like this:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
and wondered:
"Why does
Promiserun beforesetTimeout, even though the timer is0ms?"
You're not alone.
To understand this, we need to understand a few important JavaScript concepts:
- Call Stack
- Web APIs / Node.js APIs
- Callback Queue
- Macrotasks
- Microtask Queue
- Event Loop
- Promises
setTimeout()
Once these pieces fit together, asynchronous JavaScript becomes much easier to reason about.
1. Is JavaScript really single-threaded?
JavaScript code is executed by an execution environment that processes one piece of JavaScript at a time on a given thread.
That means JavaScript doesn't normally execute two JavaScript functions simultaneously on the same execution thread.
So if we write:
console.log("A");
console.log("B");
console.log("C");
the execution happens in order:
A
B
C
This is possible because JavaScript uses a Call Stack to keep track of what is currently executing.
But then we have a problem.
What happens when JavaScript needs to wait for:
- a timer?
- a network request?
- a file operation?
- a user click?
- some other asynchronous operation?
JavaScript shouldn't freeze the entire application while waiting.
This is where the runtime environment and event loop come in.
2. The Call Stack
The Call Stack is a data structure used to keep track of JavaScript execution contexts.
It follows the LIFO principle:
Last In, First Out.
Consider:
function first() {
console.log("First");
}
function second() {
console.log("Second");
}
first();
second();
When first() is called, it is placed on the stack.
Call Stack
┌─────────────┐
│ first() │
└─────────────┘
Inside first(), console.log() is executed.
Once first() finishes, it is removed from the stack.
Then second() is executed.
first()
↓
console.log()
↓
first() finishes
↓
second()
↓
console.log()
↓
second() finishes
The Call Stack is therefore responsible for what JavaScript is executing right now.
Simple definition
The Call Stack is a LIFO structure that keeps track of currently executing JavaScript functions.
3. What happens with asynchronous code?
Now consider:
console.log("Start");
setTimeout(() => {
console.log("Hello");
}, 1000);
console.log("End");
A common beginner assumption is:
Start
wait 1 second
Hello
End
But that's not what happens.
The output is:
Start
End
Hello
Why?
Because JavaScript doesn't keep the setTimeout() callback on the Call Stack for one second.
Instead, the timer is handled by the surrounding runtime environment.
In a browser, this involves Web APIs.
In Node.js, the runtime provides its own APIs and event-loop mechanisms for timers, I/O, and other asynchronous work.
4. Web APIs
When JavaScript runs inside a browser, the browser provides APIs that JavaScript can use.
These include things such as:
setTimeout()
setInterval()
fetch()
DOM events
XMLHttpRequest
These capabilities are provided by the browser environment rather than being implemented as ordinary JavaScript functions inside your program.
For example:
setTimeout(() => {
console.log("Hello");
}, 1000);
Conceptually:
JavaScript
│
│ setTimeout()
▼
Browser runtime
│
│ waits for timer
▼
Callback becomes eligible
Meanwhile, JavaScript can continue executing:
console.log("This runs immediately");
That's one of the reasons asynchronous programming is so important in JavaScript.
5. Node.js APIs
When JavaScript runs in Node.js, there is no browser providing Web APIs.
Instead, Node.js provides runtime APIs and an event-loop implementation for things such as:
- timers
- file system operations
- networking
- other I/O operations
For example:
const fs = require("fs");
fs.readFile("data.txt", "utf8", (err, data) => {
console.log(data);
});
console.log("Reading file...");
The file operation can be handled by the Node.js runtime while JavaScript continues executing other code.
So a useful mental model is:
Browser JavaScript
↓
Browser APIs
Node.js JavaScript
↓
Node.js APIs / runtime
The exact internals differ between browsers and Node.js, so it's better not to think of the browser and Node.js event loops as identical implementations.
6. Callback Queue
When an asynchronous operation finishes, the callback needs somewhere to wait until JavaScript can execute it.
This is where task queues come into the picture.
For example:
setTimeout(() => {
console.log("Timer finished");
}, 0);
Once the timer is eligible, its callback can be queued as a task.
A simplified mental model looks like:
Timer
│
▼
Task Queue
│
▼
Event Loop
│
▼
Call Stack
You may hear this queue called the Callback Queue, Task Queue, or Macrotask Queue.
Strictly speaking, the modern web platform uses the terminology tasks and microtasks rather than treating everything as one generic "callback queue."
7. What is a Macrotask?
The term macrotask is commonly used by developers when discussing tasks such as timer callbacks.
For example:
setTimeout(() => {
console.log("Hello");
}, 0);
The callback is scheduled as a task.
Other examples can include callbacks associated with:
setTimeout()
setInterval()
user events
some I/O operations
The exact task scheduling behavior depends on the runtime and API involved.
A simplified model is:
Macrotask / Task Queue
┌──────────────────────┐
│ setTimeout callback │
├──────────────────────┤
│ event callback │
├──────────────────────┤
│ another task │
└──────────────────────┘
8. What is a Microtask?
Now we get to the interesting part.
A microtask is a type of asynchronous work that is processed at specific points in the event loop, before the runtime proceeds to the next task.
Promises use the microtask queue for their reaction callbacks.
For example:
Promise.resolve().then(() => {
console.log("Promise");
});
The function passed to .then() does not execute immediately.
Instead, it is scheduled as a microtask.
Other ways to create microtasks include:
queueMicrotask(() => {
console.log("Microtask");
});
The simplified picture is:
Microtask Queue
┌─────────────────────┐
│ Promise.then() │
├─────────────────────┤
│ Promise.catch() │
├─────────────────────┤
│ queueMicrotask() │
└─────────────────────┘
9. Why do Promises run before setTimeout?
This is the question that usually makes the event loop finally "click."
Consider:
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
The output is:
Start
End
Promise
setTimeout
Let's understand why.
Step 1: Synchronous code
First:
console.log("Start");
prints:
Start
Then:
setTimeout(..., 0);
schedules a timer callback.
Next:
Promise.resolve().then(...);
schedules the Promise reaction as a microtask.
Finally:
console.log("End");
prints:
End
So the synchronous output is:
Start
End
10. What is waiting in the queues?
After the synchronous code finishes, conceptually we have:
Microtask Queue
Promise
and:
Task / Macrotask Queue
setTimeout
The runtime processes the microtask queue before moving on to the next task.
Therefore:
Promise
runs first.
Then:
setTimeout
runs.
Final output:
Start
End
Promise
setTimeout
MDN describes Promise callbacks as microtasks and setTimeout() callbacks as tasks, with microtasks processed before the next task.
11. setTimeout(..., 0) does NOT mean "run immediately"
This is an important misconception.
When you write:
setTimeout(() => {
console.log("Hello");
}, 0);
0 does not mean:
Execute this function right now.
It means the timer is configured with a zero-millisecond delay, after which the callback becomes eligible to be scheduled.
The callback still has to wait for the current JavaScript execution to finish and for the event loop to reach that task.
For example:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
Output:
A
C
B
The timer doesn't interrupt the currently running JavaScript.
12. Promises don't execute immediately either
There's another important misconception.
Some developers see:
Promise.resolve().then(() => {
console.log("Promise");
});
and think the callback runs immediately because the Promise is already resolved.
It doesn't.
Consider:
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("Normal");
Output:
Normal
Promise
The .then() callback is scheduled as a microtask.
MDN explicitly notes that Promise .then() callbacks are not called synchronously, even when the Promise is already resolved.
13. The Event Loop
The Event Loop is the mechanism that coordinates JavaScript execution with queued tasks and microtasks.
A simplified model is:
┌───────────────┐
│ Call Stack │
└───────┬───────┘
│
▼
Stack becomes empty
│
▼
┌─────────────────┐
│ Microtask Queue │
└────────┬────────┘
│
Drain microtasks
│
▼
┌─────────────────┐
│ Task Queue │
└────────┬────────┘
│
Run a task
│
▼
Drain microtasks
│
▼
Next task
The important idea is:
The runtime doesn't simply alternate between "stack" and "queue." After a task finishes, pending microtasks are processed before the next task is taken.
MDN describes the event loop as processing a task and then pending microtasks, with newly queued microtasks also being processed before moving on.
14. A More Interesting Example
Let's make things slightly harder:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
Promise.resolve().then(() => {
console.log("4");
});
console.log("5");
What will be printed?
First, synchronous code:
1
5
Then microtasks:
3
4
Then the timer:
2
So the final output is:
1
5
3
4
2
The key is not to memorize the output.
Instead, identify where each callback goes.
console.log("1")
↓
Synchronous
setTimeout(...)
↓
Task
Promise.then(...)
↓
Microtask
Promise.then(...)
↓
Microtask
console.log("5")
↓
Synchronous
15. Microtasks Can Create More Microtasks
This is where the event loop gets even more interesting.
Consider:
setTimeout(() => {
console.log("A");
}, 0);
Promise.resolve().then(() => {
console.log("B");
Promise.resolve().then(() => {
console.log("C");
});
});
Output:
B
C
A
Why?
Initially:
Microtask Queue:
B
Task Queue:
A
The microtask B runs:
B
While running B, another Promise callback is created:
Microtask Queue:
C
The runtime continues processing microtasks until the microtask queue is empty.
So:
C
runs before the timer.
Finally:
A
runs.
MDN notes that newly added microtasks are processed before the next task, which is also why continuously creating microtasks can potentially prevent the runtime from reaching later tasks.
16. A Macrotask Can Create a Microtask
Now reverse the situation:
setTimeout(() => {
console.log("A");
Promise.resolve().then(() => {
console.log("B");
});
}, 0);
setTimeout(() => {
console.log("C");
}, 0);
Output:
A
B
C
Why isn't it:
A
C
B
When the first timer runs:
A
it creates a Promise microtask.
That microtask is processed before the runtime proceeds to the next task:
A
B
C
This is one of the most useful patterns to understand when debugging asynchronous JavaScript.
17. A Complete Example
Consider this:
console.log("A");
setTimeout(() => {
console.log("B");
Promise.resolve().then(() => {
console.log("C");
});
}, 0);
Promise.resolve().then(() => {
console.log("D");
setTimeout(() => {
console.log("E");
}, 0);
});
console.log("F");
Let's trace it.
First: synchronous code
A
F
Then: microtask
D
During D, another timer is scheduled:
E
Next: first timer
B
During B, another Promise microtask is created.
So:
C
runs before the next timer.
Finally:
E
The output is:
A
F
D
B
C
E
18. The Mental Model I Use
When I see asynchronous JavaScript, I don't try to guess the output immediately.
I ask three questions:
Question 1: Is this synchronous?
If yes, it runs on the Call Stack immediately.
Question 2: If it's asynchronous, which queue does it use?
For example:
Promise.then()
↓
Microtask
setTimeout()
↓
Task / Macrotask
Question 3: Are there microtasks waiting?
After a task completes, microtasks are processed before the next task.
This makes complicated examples much easier to solve.
19. Quick Comparison
| Feature | Call Stack | Microtask Queue | Task / Macrotask Queue |
|---|---|---|---|
| Purpose | Execute current JS | Schedule microtasks | Schedule tasks |
| Examples | Function calls | Promise callbacks | setTimeout() |
| Executes | Immediately while active | After current task/stack | During a later event-loop iteration |
| Priority | Current execution | Before next task | After microtasks |
| Can contain Promise callbacks? | No | Yes | No |
| Can contain timer callbacks? | No | No | Yes |
20. The Most Important Ordering Rule
If you remember only one thing from this article, remember this:
Synchronous JavaScript
↓
Microtasks
↓
Next Task
↓
Microtasks
↓
Next Task
Or, more practically:
Promise.then()
↓
before
↓
setTimeout()
when both are scheduled during the same synchronous turn.
This is why:
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
produces:
Promise
Timeout
21. Common Mistakes
Mistake 1: "0ms means immediate"
setTimeout(fn, 0);
❌ Not immediate.
It schedules a task that can run after the timer becomes eligible and the event loop gets to it.
Mistake 2: "Promises are synchronous"
Promise.resolve().then(fn);
❌ The .then() callback is asynchronous.
It is scheduled as a microtask.
Mistake 3: "The event loop executes everything at once"
❌ JavaScript execution on a given thread proceeds one job/task at a time.
The event loop coordinates when queued work gets a chance to execute.
Mistake 4: "Every asynchronous callback goes into the same queue"
❌ Not quite.
The runtime distinguishes between tasks and microtasks, and their scheduling rules differ.
Promises use microtasks, while timer callbacks such as setTimeout() are tasks.
22. Final Cheat Sheet
CALL STACK
→ Where current JavaScript executes.
WEB APIs
→ Browser-provided asynchronous capabilities.
NODE.JS APIs
→ Runtime APIs for Node.js timers, I/O, networking, etc.
CALLBACK
→ A function that can be invoked later.
TASK / MACROTASK
→ A scheduled unit of work such as a timer callback.
MICROTASK
→ High-priority queued work processed before the next task.
MICROTASK QUEUE
→ Where Promise reaction callbacks are scheduled.
EVENT LOOP
→ Coordinates execution of tasks and microtasks.
PROMISE
→ Represents the eventual completion or failure of an asynchronous operation.
setTimeout()
→ Schedules a timer callback as a task.
PROMISE.then()
→ Schedules a reaction callback as a microtask.
And the core ordering:
┌─────────────────────┐
│ Synchronous Code │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Microtask Queue │
│ Promise.then() │
│ Promise.catch() │
│ queueMicrotask() │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Task Queue │
│ setTimeout() │
│ events / other tasks│
└──────────┬──────────┘
↓
Microtasks again
↓
Next task
Once you understand Call Stack → Runtime APIs → Task Queue → Microtask Queue → Event Loop, you can predict most common JavaScript asynchronous output questions without memorizing them.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.