JavaScript Foundations: DOM Events, the Event Loop, and Async Execution
June 5, 2026
In today’s article, I want to focus on some of the most important JavaScript concepts for modern front-end development:
- How JavaScript executes code using the call stack
- Why async code prints in surprising orders
- The difference between microtasks and macrotasks
- Why
Promise.then()can run beforesetTimeout(..., 0)
This topic initially feels confusing because the output order can seem random. But once I built a mental model for how JavaScript schedules work, it became much easier to predict what would happen before even running the code.
These concepts are extremely important for Angular, RxJS, timers, HTTP requests, UI rendering, and debugging asynchronous behavior.
The Mental Model: JavaScript Is Single-Threaded
JavaScript executes one thing at a time.
The easiest way to think about this is:
JavaScript has one chef in one kitchen.
That chef can only cook one meal at a time.
This is called the call stack.
The Call Stack
The call stack is where JavaScript executes synchronous code.
Example:
console.log("A");
console.log("B");
console.log("C");
Output:
A
B
C
This was straightforward because JavaScript simply executes line by line.
The important takeaway:
Synchronous code always runs first.
That became one of the biggest lessons from today.
Browser APIs and Timers
Some tasks take time:
setTimeout- DOM events
- HTTP requests
- fetch calls
Instead of blocking JavaScript, the browser handles them separately.
Example:
setTimeout(() => {
console.log("timer");
}, 1000);
JavaScript essentially says:
“Let me know when this is ready.”
Then it continues running the rest of the code.
This led to one important realization:
setTimeout(..., 0)does not mean immediate.
It means:
“Run this later when the stack is empty.”
Tasks vs Microtasks
One of the most important ideas from today was understanding queue priority.
There are two important queues:
Task Queue (Macrotask Queue)
Examples:
setTimeout- DOM events
- timers
Example:
setTimeout(() => {
console.log("timeout");
}, 0);
Microtask Queue
Examples:
Promise.then()queueMicrotask()
Example:
Promise.resolve().then(() => {
console.log("promise");
});
The key idea:
Microtasks run before macrotasks.
This explains why promise callbacks often run before timers.
Mental model:
Call Stack
↓
Microtasks (Promises)
↓
Tasks / Macrotasks (setTimeout)
This single diagram unlocked most of the confusing examples.
Example 1: Promise vs Synchronous Code
console.log("A");
Promise.resolve().then(() => {
console.log("B");
});
console.log("C");
Before running this code, I predicted the output.
Output:
A
C
B
Why?
- Synchronous code runs first
A
C
- The stack empties
- Microtasks execute
B
A simple explanation:
Promise.then()creates the microtask, and the callback executes after synchronous code completes.
Example 2: setTimeout Is Not Immediate
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");
Output:
1
3
2
Why?
- Sync code runs first
1
3
- No microtasks exist
- Task queue runs
2
This reinforced:
Sync first → microtasks → tasks
Example 3: Promise vs setTimeout
console.log("start");
setTimeout(() => {
console.log("timeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise");
});
console.log("end");
Output:
start
end
promise
timeout
Why?
- Sync code runs first
start
end
- Promise microtasks execute
promise
- Task queue executes
timeout
This finally answered an interview question that could feel confusing:
Why can
Promise.then()run beforesetTimeout(..., 0)?
Answer:
Promise.then()runs in the microtask queue, which has higher priority than the task/macrotask queue used bysetTimeout().
Example 4: Multiple Promises
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
Promise.resolve().then(() => {
console.log("4");
});
console.log("5");
Prediction:
1
5
3
4
2
Correct.
Why?
- Synchronous code executes first
1
5
- Microtasks run in insertion order
3
4
- Macrotasks run afterward
2
This example helped reinforce that:
Promises maintain their order inside the microtask queue.
Example 5: Nested Async Behavior
console.log("start");
setTimeout(() => {
console.log("timeout");
Promise.resolve().then(() => {
console.log("promise");
});
}, 0);
console.log("end");
Prediction:
start
end
timeout
promise
Correct.
Why?
- Sync code runs first
start
end
- The timeout callback runs
timeout
- The promise is created inside the timeout callback
- That promise becomes a microtask after the timeout work finishes
promise
This helped me understand an important nuance:
Microtasks have higher priority, but they only exist after they are created.
What Initially Threw Me Off
The biggest thing that could have been confusing was:
Why does
Promise.then()sometimes run beforesetTimeout(..., 0)?
At first glance, setTimeout(..., 0) feels like it should happen immediately.
But the better mental model became:
“Run this later when the stack is empty.”
Promises felt less confusing once I understood:
They are async too — just higher-priority async.
The biggest unlock was remembering:
Sync first → microtasks → tasks
That one sentence solved almost every example.
Angular Connection
This matters directly in Angular.
Example:
this.http.get("/users").subscribe(() => {
console.log("data");
});
console.log("done");
Output:
done
data
Why?
HTTP requests are asynchronous.
Angular and RxJS depend heavily on event-loop behavior.
The stronger my understanding of JavaScript execution order becomes, the easier Angular debugging and interview questions become.
Final Takeaways
The biggest ideas from today:
- JavaScript is single-threaded
- Synchronous code runs first
- Promises are microtasks
setTimeout()uses the task/macrotask queue- Microtasks run before tasks
setTimeout(..., 0)is not immediate
The retention phrase from today:
Sync first → microtasks → tasks
