In the previous lessons, we explored different ways to write asynchronous code in JavaScript, such as:
- Using callbacks
- Working with Promises
- Using async / await
- Scheduling work using setTimeout
All these approaches help us deal with tasks that do not complete immediately, such as API calls, timers, or user interactions.
However, an important question often remains unanswered:
In what order are these asynchronous operations actually executed?
More specifically:
- Does JavaScript treat
setTimeoutandPromisecallbacks the same way? - Do they have the same priority?
- Why do some asynchronous operations run earlier even if they appear later in the code?
To answer these questions, we need to look deeper into how the JavaScript runtime, event loop, and task queues work together.
A Simple Starting Example
Let us begin with a very simple example that contains three statements:
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
At first glance, this code may look straightforward. You might think:
- First,
console.log(1)runs. - Then
setTimeoutruns after0milliseconds. - Then
console.log(3)runs.
So maybe the output should be:
1 2 3
But that is not what happens.
Actual Output
1 3 2
Let’s understand why this happens.
Why setTimeout(..., 0) Does Not Run Immediately
Even though we specify 0 milliseconds in setTimeout, it does not mean “run immediately”.
What it really means is:
“Execute this callback after the current execution stack is empty and once the task is picked from the task queue.”
Key difference between the two statements:
console.log(1)is executed immediately because it goes directly into the call stack.setTimeoutschedules its callback and sends it to the task queue, even if the delay is0.
Because of this behavior:
- The call stack must completely finish execution first.
- Only after that can the event loop move tasks from the queue into the stack.
That is why the order becomes:
1 → 3 → 2
Replacing setTimeout with a Promise
Now let’s slightly modify the example by replacing setTimeout with a Promise.
console.log(1);
Promise.resolve().then(() => {
console.log(2);
});
console.log(3);
Output:
1 3 2
The output remains the same — but the reason is different.
Why does this happen?
- The synchronous statements (
console.log(1)andconsole.log(3)) are executed immediately. - The promise callback is asynchronous, so it does not execute right away.
However, the promise callback is not placed in the same queue as setTimeout.
This brings us to an extremely important concept.
Not All Asynchronous Tasks Are Equal
Even though both setTimeout and Promise are asynchronous, they do not have the same priority.
JavaScript internally uses different queues for different types of tasks.
Broadly speaking:
- Promises go into the microtask queue
- setTimeout / setInterval go into the macrotask (task) queue
And microtasks always have higher priority than macrotasks.
This is why promises usually execute before setTimeout, even if the timeout appears earlier in the code.
Combining setTimeout and Promise
Now let’s examine a slightly more complex example where both appear together.
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
Promise.resolve().then(() => {
console.log(3);
});
console.log(4);
Step-by-step execution:
console.log(1)runs immediately.setTimeoutis registered and its callback is placed in the task queue.- The promise callback is placed in the microtask queue.
console.log(4)executes immediately.- The call stack is now empty.
- The event loop checks the microtask queue first, so
console.log(3)runs. - Only after all microtasks are finished does the event loop process the task queue, executing
console.log(2).
Final Output:
1 4 3 2
Why Does This Happen?
This happens because JavaScript assigns different priorities to different types of asynchronous work.
- Microtasks (Promises, MutationObserver)
These have higher priority and are executed immediately after the current call stack is empty. - Macrotasks (setTimeout, setInterval, I/O events)
These run only after all microtasks are completed.
This priority system ensures predictable and efficient execution of asynchronous code.
Revisiting the Event Loop (A Deeper View)
Earlier, you learned that JavaScript relies on:
- The Call Stack
- The Task Queue
- The Event Loop
However, this is a simplified view.
In reality, modern browsers have multiple task queues, not just one.
Multiple Task Queues in Browsers
Browsers may maintain separate queues for different types of tasks, such as:
- Timers (
setTimeout,setInterval) - User interaction events (clicks, keyboard input)
- Network operations (HTTP responses)
- Microtasks (Promises)
Each browser is free to implement this differently.
The ECMAScript specification only states that:
A browser can have one or more task queues, and tasks within the same queue must be executed in order.
This means:
- Tasks within the same queue always preserve order.
- Tasks across different queues can be interleaved depending on priority.
How the Event Loop Chooses Tasks
The event loop operates roughly as follows:
- Check if the call stack is empty.
- If yes, pick the next available task from a queue.
- Execute it completely.
- Process all microtasks before moving on.
- Repeat.
Important rule:
Tasks from the same queue must be executed in the order they were added.
This guarantees predictability within each task category.
Rendering and the Browser’s Rendering Pipeline
So far, we’ve discussed JavaScript execution. But browsers also need to render the UI.
Rendering happens:
- Roughly 60 times per second
- Which means once every ~16 milliseconds
This frequency ensures smooth animations and responsive interactions.
However:
- Rendering can only happen when the call stack is empty
- If JavaScript blocks the thread for more than ~16ms, the browser cannot repaint
This leads to:
- Janky animations
- UI freezing
- Poor user experience
Why Long Tasks Are Dangerous
If a single task runs longer than 16ms:
- The rendering pipeline must wait.
- Frames are dropped.
- Animations stutter.
- User interactions feel unresponsive.
This is why modern applications try to:
- Break heavy tasks into smaller chunks
- Use
requestAnimationFrame - Offload work to Web Workers when possible
Summary
- JavaScript executes code using a call stack, task queues, and an event loop.
setTimeoutandPromisecallbacks are asynchronous, but they have different priorities.- Promises (microtasks) always run before timers (macrotasks).
- Browsers may have multiple task queues, and tasks in the same queue preserve order.
- Rendering happens roughly every 16ms, but only when the main thread is free.
- Long-running JavaScript blocks rendering and causes performance issues.
