I've used promises for everyday programming. I knew how to use promises, but I never questioned how JavaScript actually handles promises and asynchronous programming.
Take this one for example:
function main() {
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
new Promise(resolve => {
console.log("C");
resolve("D");
}).then(value => {
console.log(value);
});
console.log("E");
}
main();
If you run this program, the output will be ACEDB. But why? How?
JavaScript is single-threaded. Its main thread is responsible for executing JavaScript code. When a function is invoked, it goes to the call stack and executes synchronously. The call stack is a stack data structure that uses the Last In, First Out (LIFO) method.
When the program calls main(), it pushes it onto the call stack. Then it reads console.log("A") and prints A.
Then comes setTimeout(). The timer is handled by the JavaScript environment (such as the browser or Node.js), rather than being executed by the JavaScript call stack itself. Once the timer is ready, its callback is pushed to the task queue that can be executed later.
Then comes the Promise.
This is where things get interesting.
The Promise executor function itself is synchronous. It runs immediately on the call stack. So:
new Promise(resolve => {
console.log("C");
resolve("D");
})
prints C immediately.
Calling resolve("D") fulfills the Promise, but the .then() callback does not execute immediately. Promise reactions are queued in the microtask queue, which will run after the current synchronous code finishes.
So at this point, the .then() callback is waiting in the microtask queue.
Then the thread reaches console.log("E") and prints E.
At this point, the main() function finishes and the call stack becomes empty.
Now the event loop can continue processing asynchronous work. The event loop checks when the call stack is empty and coordinates when queued asynchronous callbacks can run. The important part is that microtasks have priority over tasks. So the Promise's .then() callback runs first:
console.log("D");
That's why D is printed before B.
After the microtask queue is processed, the timer callback can run, so:
console.log("B");
prints B.
That's how we get:
A
C
E
D
B
or:
ACEDB
The interesting thing I learned here is that the Promise executor itself wasn't asynchronous. C was printed synchronously. What was asynchronous was the execution of the .then() callback after the Promise was resolved.
The simplified flow looks like this:
Call Stack
↓
Execute synchronous JavaScript
↓
Call stack becomes empty
↓
Process Microtasks
↓
Process next Task
In this example:
A → C → E → D → B
A, C, and E happen during normal synchronous execution.
D is executed as a microtask.
B is executed later as the timer task.
That's the part of JavaScript's asynchronous workflow that I hadn't really thought about before. I had been using Promises every day, but understanding what happens behind the scenes makes the behavior much easier to reason about.
Top comments (0)