Why does this line of code with 'await' trigger microtask queue processing? - javascript

The following quotes are my primary references for understanding microtask queue processing:
Microtasks (which promises use) are processed when the JS stack empties.
- Jake Archibald
That doesn't make sense to me.
One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification). After this macrotask has finished, all available microtasks will be processed, namely within the same go-around cycle.
- Stack Overflow
Now, regarding line 9 (**) in the following snippet:
From stepping through this snippet w/ debugger, the execution stack does not appear empty when these .then( callback ) microtasks are processed/executed.
Are regular functions like f2() considered a task (aka macrotask)? (when it returns it's an event loop nextTick() and the microtask queue is processed)
Why are microtasks executing when the JS stack is not empty?
function f2() {
let x = new Promise( (resolve, reject) => { resolve( () => {console.log('howdy')} ) })
return x
}
async function f1(){
let y = Promise.resolve().then(() => { console.log('yo1')})
console.log('yo2')
let r2awaited = await f2() //** 'yo0' and 'yo1' log when the interpreter hits this line.
return r2awaited
}
async function start(){
let y = Promise.resolve().then(() => { console.log('yo0')})
let xx = await f1()
console.log('main call return:')
console.log(xx)
}
start()
Edit: Another peculiar finding - when you add xx() right after console.log(xx) on line 17, the stack is completely cleared prior to executing the microtasks.
The call stack 1 step prior to microtask queue processing:
Then the immediate next step.
Between these two steps, the microtask queue was processed.
Does the call stack clear [under the hood] between these steps^?
And then is a new call stack created according to the required lexical environment(s) for code after the await [expression]?
Edit: At the time of posting this, I was not aware that everything below the -----(async)----- line in the chrome debugger's call stack was part of a 'fake stack'.
This 'fake stack' is presented for async debugging in a way consistent with sync debugging.
Only the elements above this -----(async)----- line are part of the real main thread call stack.

"Microtasks (which promises use) are processed when the JS stack empties." -Jake Archibald (doesn't make sense to me)
The "call stack" is the list of things that are currently executing:
function foo() {
debugger;
console.log('foo');
}
function bar() {
foo();
debugger;
}
bar();
When we hit the first debugger statement, the script is still executing, as is bar, as is foo. Since there's a parent-child relationship, the stack is script > bar > foo. When we hit the second debugger statement, foo has finished executing, so it's no longer on the stack. The stack is script > bar.
The microtask queue is processed until it's empty, when the stack becomes empty.
"One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification). After this macrotask has finished, all available microtasks will be processed, namely within the same go-around cycle." - stackoverflow
Edit: I kept reading "macrotask" above as "microtask". There isn't really such a thing as a macrotask queue in the browser, it's just a task queue.
Although it's true that there's a microtask processing point after processing a task, it's only really there to handle specifications that queue tasks to queue microtasks, without calling into JS first. Most of the time, the microtask queue is emptied when the JS stack empties.
From stepping through this snippet w/ debugger, the execution stack does not appear empty when these .then( callback ) microtasks are processed/executed.
The stack will never be empty while callbacks are being executed, since the callback itself will be on the stack. However, if this is the only thing on the stack, you can assume the stack was empty before this callback was called.
Chrome's devtools tries to be helping in maintaining an "async" stack, but this isn't the real stack. The real stack is everything before the first "async" line.
Are regular functions like f2() considered a task
Being a task or a microtask isn't a property of a function. The same function can be called within a task, a microtask, and other parts of the event loop such as rendering. Eg:
function foo() {}
// Here, I'll call foo() as part of the current task:
foo();
// Here, I'll let the browser call foo() in a future task:
setTimeout(foo);
// Here, I'll let the browser call foo() in a microtask:
Promise.resolve().then(foo);
// Here, I'll let the browser call foo() as part of the render steps:
requestAnimationFrame(foo);
In your example, f2 is not called within a microtask. It's kinda like this:
function three() {}
function two() {}
async function one() {
await two();
three();
}
one();
Here, one() is called within the task that executed the script. one() calls two() synchronously, so it runs as part of the same task. We then await the result of calling two(). Because we await, the rest of the function runs in a microtask. three() is called, so it runs in the same microtask.

Related

Does Promise add the call backfunction to microtask queue?

I read some files about the microtask and macrotask queue. I know that the callback function of a resolved promise is added to the microtask, same as the other sync tasks. But the following code:
fetch=() => new Promise ((resolve) => resolve ({m: 121}))
function x() {
let d = 3;
fetch().then((data) => {
d += data.m;
console.log("b", d);
});
console.log("a", d);
}
x();
gives a result of
a 3
b 124
IAs I thought, since fetch here fetches a "local" file and it takes no time, so the callback function is immediately added to the microtask queue, and then console.log("a", d); is added to the microtask queue. If so, the result should be
b 124
a 124
So what is the reason for the real result? Why the call back function is executed after console.log("a", d);?
Whether or not a callback is added to the microtask queue or the callback queue has nothing to do with how long it takes to run. The Promise callbacks are always added to the microtask queue. Regardless of how long it takes. And, the queue is cleared, after the call stack is empty. In your example, the function x has to complete execution before your Promise callback can run. So, a is logged first.
Read more about the event loop here. But in a nutshell, browser API callbacks go in the callback queue, and higher priority callbacks like the Promise callbacks go in the microtask queue. The queues are cleared only after the call stack is empty.
This can lead to puzzling behaviors like the following code running forever since the call stack never becomes empty.
let flag = true;
setTimeout(() => flag = false, 200)
while (flag) console.log('hello')

Difference between async await in python vs JavaScript

Note: this is not about multi threading or multi processing. This question is regarding a single process and single thread.
Python async.io and JavaScript async both are single thread concepts.
In python, async.io, we can use async await keywords to create a function so that when this function is invoked multiple times (via gather) they get executed concurrently. The way it works is that when an await keyword is encountered, other tasks can execute. As explained here we apply the async await keywords to function that we would like to execute concurrently. However while these tasks are running concurrently, the main thread is blocked.
In JavaScript async has evolved from callbacks, promise, async/await. In the main program, when async is encountered, then the function is sent to the event loop (where the function execution begins) and the main thread can continue working. Any subsequent async function also gets added to the event loop. Inside the event loop when the function execution encountered an await then other function is given a chance to execute untill await in encountered.
To get this behaviour in python, that is - allow main thread to continue while executing child tasks the only option is multithreading/multiprocessing. Because once we start the child thread/process, and untill we call .join the main thread is not blocked.
Is there anyway by which the python's async.io can make the main thread non blocking? If not, then is this the fundamental difference between async concept in JavaScript and python?
when async is encountered, then the function is sent to the event loop and the main thread can continue working.
This is close, but not quite right. In Javascript, execution won't stop until the callstack has been emptied - the await keyword will suspend the execution of a particular function until an event triggers, and in the mean time, control returns to its caller. This means the first part of any async function will execute as soon as it is called (it's not immediately put into the event loop), and will only pause as soon as an await is hit.
To get this behaviour in python, that is - allow main thread to continue while executing child tasks the only option is multithreading/multiprocessing.
The difference here is that by default, Javascript always has an event loop and python does not. In other words, python has an on/off switch for asynchronous programming while Javascript does not. When you run something such as loop.run_forever(), you're basically flipping the event loop on, and execution won't continue where you left off until the event loop gets turned back off. (calling it a "thread" isn't quite the right word here, as it's all single-threaded, as you already acknowledged. Instead, we generally call each task that we queue up, well, a "task")
You're asking if there's a way to let your code continue execution after starting up the event loop. I'm pretty sure the answer is no, nor should it be needed. Whatever you want to execute after the event loop has started can just be executed within the event loop.
If you want your python program to act more like Javascript, then the first thing you do can be to start up an event loop, and then any further logic can be placed within the first task that the event loop executes. In Javascript, this boiler plate essentially happens for you, and your source code is effectively that first task that's queued up in the event loop.
Update:
Because there seems to be some confusion with how the Javascript event loop works, I'll try to explain it a little further.
Remember that an event loop is simply a system where, when certain events happen, a block of synchronous code can be queued up to run as soon as the thread is not busy.
So let's see what the event loop does for a simple program like this:
// This async function will resolve
// after the number of ms provided has passed
const wait = ms => { ... }
async function main() {
console.log(2)
await wait(100)
console.log(4)
}
console.log(1)
main()
console.log(3)
When Javascript begins executing the above program, it'll begin with a single task queued up in it's "run these things when you're not busy" queue. This item is the whole program.
So, it'll start at the top, defining whatever needs to be defined, executes console.log(1), call the main function, enters into it and runs console.log(2), calls wait() which will conceptually cause a background timer to start, wait() will return a promise which we then await, at which point we immediately go back to the caller of main, main wasn't awaited so execution continues to console.log(3), until we finally finish at the end of the file. That whole path (from defining functions to console.log(3)) is a single, non-interruptible task. Even if another task got queued up, Javascript wouldn't stop to handle that task until it finished this chunk of synchronous logic.
Later on, our countdown timer will finish, and another task will go into our queue, which will cause our main() function to continue execution. The same logic as before applies here - our execution path could enter and exit other async functions, and will only stop when it reaches the end of, in this case, the main function (even hitting an await keywords doesn't actually make this line of syncrounous logic stop, it just makes it jump back to the caller). The execution of a single task doesn't stop until the callstack has been emptied, and when execution is continuing from an async function, the first entry of the callstack starts at that particular async function.
Python's async/await follows these same rules, except for the fact that in Python, the event loop isn't running by default.
javascript
const wait = async (s) => {
setTimeout(() => {
console.log("wating " + s + "s")
}, s * 1000)
}
async function read_file() {
console.log("initial read_file sleep(2.1)")
await wait(2)
console.log("read_file 1/2 wait(2)")
await wait(0.1)
console.log("read_file 2/2 wait(0.1)")
}
async function read_api() {
console.log("initial read_api wait(2)")
await wait(2)
console.log("read_api whole wait(2)")
}
read_file()
console.log("does not block")
read_api()
console.log("the second time, won't block")
// initial read_file sleep(2.1)
// does not block
// initial read_api wait(2)
// the second time, won't block
// read_file 1/2 wait(2)
// read_api whole wait(2)
// read_file 2/2 wait(0.1)
// !!! Wait a moment
// wating 0.1s
// wating 2s
// wating 2s
python
import asyncio
async def read_file():
print("initial read_file asyncio.sleep(2 + 0.1)")
await asyncio.sleep(2)
print("read_file 1/2 asyncio.sleep(2)")
await asyncio.sleep(0.1)
print("read_file 2/2 asyncio.sleep(0.1)")
async def read_api():
print("initial read_api asyncio.sleep(2)")
await asyncio.sleep(2)
print("read_api whole asyncio.sleep(2)")
async def gather():
await asyncio.gather(
asyncio.create_task(read_file()),
asyncio.create_task(read_api()))
asyncio.run(gather())
"""
initial read_file asyncio.sleep(2.1)
initial read_api asyncio.sleep(2)
!!! Wait a moment
read_file 1/2 asyncio.sleep(2)
read_api whole asyncio.sleep(2)
read_file 2/2 asyncio.sleep(0.1)
"""
await scope:
javascript: After the method is executed, wait for the Promise to resolve
await wait(2) Just wait(2) inside is guaranteed to be synchronous (or wait)
python: Suspend method for other methods to execute
await asyncio.sleep(2) Method read_file will release resources and suspend
btw, javascript's await/async is just Promise syntactic sugar

How Does the JavaScript Interpreter add Global Statements to the Event Queue?

I am not sure how statements in the global scope are placed into the JavaScript event queue. I first thought that the interpreter went through and added all global statements into the event queue line by line, then went and executed each event, but that logic does not line up with the example given below. How does the JavaScript interpreter add global statements to the event queue, and why is the output from the two examples given below different?
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() => {resolve("1")}, 0)
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
The console output to the above code is
1
2
Now consider this example. Here, the difference is on the body of the promise callback, as there is a nested setTimeout
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
The console output to the above code is
2
1
What I don't understand is the order in which things are added to the event queue. The first snippet implies that the promise p will run, and then during its execution, resolve is put in the event queue. Once all of p's stack frames are popped, then resolve is run. After than p.then(...) is run, and finally the last console.log("2");
In the second example, somehow the number 2 is being printed to the console before the number 1. But would things not be added to the event queue in this order
1.) p
2.) setTimeout( () => {resolve("1")}, 0)
3.) resolve("1")
4.) p.then(...)
5.) console.log("2")
I clearly have some sort of event queue logic wrong in my head, but I have been reading everything I can and I am stuck. Any help with this is greatly appreciated.
There are several confusing things in your question that I think show some misconceptions about what is happening so let's cover those initially.
First, "statements" are not ever placed into the event queue. When an asynchronous task finishes running or when it is time for a timer to run, then something is inserted in the event queue. Nothing is in the queue before that. Right after you call setTimeout(), before the time has come for the setTimeout() to fire there is nothing in the event queue.
Instead, setTimeout() runs synchronously, configures a timer in the internals of the JS environment, associates the callback function you passed to setTimeout() to that timer and then immediately returns where JS execution continues on the next line of code. Sometime later when the time has been reached for the timer to fire and control has returned back to the event loop, the event loop will call the callback for that timer. The internals of exactly how this works vary a bit according to which Javascript environment it is, but they all have the same effect relative to other things going on in the JS environment. In nodejs, for example, nothing is ever actually inserted into the event queue itself. Instead, there are phases of the event loop (different things to check to see if there's something to run) and one of the phases is to check to see if the current time is at or after the time that the next timer event is scheduled for (the soonest timer that has been scheduled). In nodejs, timers are stored in a sorted linked list with the soonest timer at the head of the list. The event loop compares the current time with the timer on the timer at the head of the list to see if its time to execute that timer yet or not. If not, it goes about its business looking for other types of events in the various queues. If so, it grabs the callback associated with that timer and calls the callback.
Second, "events" are things that cause callback functions to get called and the code in that callback function is executed.
Calling a function that may then cause something to be inserted into the event queue, either immediately or later (depending upon the function). So, when setTimeout() is executed, it schedules a timer and some time later, it will cause the event loop to call the callback associated with that timer.
Third, there is not just a single event queue for every type of event. There are actually multiple queues and there are rules about what gets to run first if there are multiple different types of things waiting to run. For example, when a promise is resolved or rejected and thus has registered callbacks to call, those promise jobs get to run before timer related callbacks. Promises actually have their own separate queue for resolved or rejected promises waiting to call their appropriate callbacks.
Fourth, setTimeout(), even when given a 0 time, always calls its callback in some future tick of the event loop. It never runs synchronously or immediately. So, the rest of the current thread of Javascript execution always finishes running before a setTimeout() callback ever gets called. Promises also always call .then() or .catch() handlers after the current thread of execution finishes and control returns back to the event loop. Pending promise operations in the event queues always get to run before any pending timer events.
And to confuse things slightly, the Promise executor function (the callback fn you pass as in new Promise(fn)) does run synchronously. The event loop does not participate in running fn there. new Promise() is executed and that promise constructor immediately calls the executor callback function you passed to the promise constructor.
Now, lets look at your first code block:
let handleResolved = (data) => {
console.log(data);
}
let p = new Promise((resolve, reject) => {
setTimeout(() => {resolve("1")}, 0)
});
p.then(handleResolved);
setTimeout(() => {
console.log("2");
}, 0);
In order, here's what this does:
Assign a function to the handleResolved variable.
Call new Promise() which immediately and synchronously runs the promise executor callback you pass to it.
That executor callback, then calls setTimeout(fn, 0) which schedules a timer to run soon.
Assign the result of the new Promise() constructor to the p variable.
Execute p.then(handleResolved) which just registers handleResolved as a callback function for when the promise p is resolved.
Execute the second setTimeout() which schedules a timer to run soon.
Return control back to the event loop.
Shortly after returning control back to the event loop, the first timer you registered fires. Since it has the same execution time as the 2nd one you registered, the two timers will execute in the order they were originally registered. So, the first one calls its callback which calls resolve("1") to cause the promise p to change its state to be resolved. This schedules the .then() handlers for that promise by inserting a "job" into the promise queue.
That job will be run after the current stack frame finishes executing and returns control back to the system.
The call to resolve("1") finishes and control goes back to the event loop.
Because pending promise operations are served before pending timers, handleResolved(1) is called. That functions runs, outputs "1" to the console and then returns control back to the event loop.
The event loop then calls the callback associated with the remaining timer and "2" is output to the console.
What I don't understand is the order in which things are added to the event queue. The first snippet implies that the promise p will run, and then during its execution, resolve is put in the event queue. Once all of p's stack frames are popped, then resolve is run. After than p.then(...) is run, and finally the last console.log("2");
I can't really respond directly to this because this just isn't how things work at all. Promises don't "run". The new Promise() constructor is run. Promises themselves are just notification machines that notify registered listeners about changes in their state. resolve is not put in the event queue. resolve() is a function that gets called and changes the internal state of a promise when it gets called. p doesn't have stack frames. p.then() is run immediately, not later. It's just that all that p.then() does is register a callback so that callback can then be called later. Please see the above 1-11 steps for the sequence of how things work.
In the second example, somehow the number 2 is being printed to the console before the number 1. But would things not be added to the event queue in this order
In the second example, you have three calls to setTimeout() where the third one is nested inside the first one. This is what changes your timing relative to the first code block.
We have mostly the same steps as the first example except that instead of this:
setTimeout(() => {resolve("1")}, 0)
you have this:
setTimeout(() = > {setTimeout(() => {resolve("1")}, 0)}, 0);
This means that the promise constructor is called and this outer timer is set.
then, the rest of the synchronous code runs and the last timer in the code block is then set. Just like in the first code block, this first timer will get to call its callback before the second one. But, this time the first one just calls another setTimeout(fn, 0). Since timer callbacks are always executed in some future tick of the event loop (not immediately, even if the time is set to 0), that means that all the first timer does when it gets a chance to run is schedule another timer. Then, the last timer in the code block gets it's turn to run and you see the 2 in the console. Then, when that's done, the third timer (the one that was nested in the first timer) gets to run and you see the 1 in the console.
If we break down the second case so that each function is on its own we end up with
const handleResolved = (data) => {
console.log(data);
}
const promiseBody = (resolve, reject) => setTimeout( innerPromiseTimeout, 0, resolve );
const innerPromiseTimeout = (resolve) => setTimeout( resolveWith1, 0, resolve );
const resolveWith1 = (resolve) => resolve("1");
const timeoutLog2 = () => {
console.log("2");
};
// beginning of execution
// timers stack: [ ]
// promiseBody is executed synchronously
let p = new Promise( promiseBody );
// timers stack: [ innerPromiseTimeout ]
// this will happen only after resolveWith1 is called
p.then( handleResolved );
// timers stack: [ innerPromiseTimeout ]
setTimeout( timeoutLog2, 0 );
// timers stack: [ innerPromiseTimeout, timeoutLog2 ]
// some time later, innerPromiseTimeout is called
// timers stack: [ timeoutLog2, resolveWith1 ]
// timeoutLog2 is called
// timers stack: [ resolveWith1 ]
// resolveWith1 is called and then is executed in next microtask checkpoint
// timers stack: [ ]
Also note that setTimeout still has a minimum of 1ms in Chrome (they will soon remove it, but for the time being, it's there), so don't assume setTimeout(fn,0) will execute as the next task

How does the JS event loop behave when a Promise's resolution depends on setTimeout?

console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
function three() {
return new Promise(resolve => {
setTimeout(() => {
return new Promise(resolve => resolve('3'))
},0)
})
}
three().then(result => console.log(result))
console.log('4')
This code snippet outputs 1 4 2
This is the behavior I would expect based on my understanding of javascript's event loop and concurrency model. But it leaves me with some lingering questions.
Before getting to those questions, I'll first break down my understanding of this code snippet.
Why code outputs 1
no explanation needed
Why code outputs 4
the callback that outputs 2 gets loaded into the event queue (aka macro task queue) after 0ms, but doesn't get executed until the main call stack is emptied.
even if three was a promise that was immediately resolved, its code is loaded into the job queue (aka microtask queue) and wouldn't be executed until the main call stack is emptied (regardless of the contents of the event queue)
Why code outputs 2
after console.log(4) the main call stack is empty and javascript looks for the next callback to load on the main stack. It's pretty safe to assume that at this point, some "worker thread" had already put the callback function that outputs 2 onto the macro task queue. This gets loaded onto the stack and 2 is output.
Why code does NOT output 3
This is where its a little blurry for me. The function three returns a promise that is then-ed in the main thread. Callback functions passed through then are loaded onto microtask queue and executed before the next task in the macrotask queue. So while you might think it'll run before the callback that logs 2, its actually theoretically impossible for it to run at all. That's because the Promise is only resolved via the callback function of its setTimeout, and that callback function (because of setTimeout) would only run if the main execution thread (the same thread that's waiting for the promise to resolve) is empty.
Why does this bother me
I'm trying to build a complete theoretical mental model of how javascript handles concurrency. One of the missing pieces in that model is the relationship between network requests, promises, and the event loop. Take the above code snippet, and suppose I replace three's setTimeout with some sort of network request (a very common thing in async web development). Assuming that the network request behaves similarly to setTimeout, in that when the "worker thread" is done, a callback is pushed to the macro task queue, it's hard for me to understand how that callback even gets executed. But this is something that happens literally all the time.
Can someone help me understand? Do I have any missing gaps in my current understanding of js concurrency? Have I made an incorrect assumption? Does any of this actually make any sense? lol
Why code does NOT output 3
In this code:
function three() {
return new Promise(resolve => {
setTimeout(() => {
return new Promise(resolve => resolve('3'))
},0)
})
}
three().then(result => console.log(result))
You never resolve the first Promise that three() creates. Since that's the one that is returned form three(), then the .then() handler in three().then(...) is never called.
You do resolve the promise created inside the timer, but you're returning that promise only to the timer callback which does nothing.
If you change your code to this:
function three() {
return new Promise(resolve => {
setTimeout(() => {
resolve('3');
},0)
})
}
three().then(result => console.log(result))
Then, you would see the 3get output.
So, this doesn't have anything to do with the event loop or how it works. It has to do with not resolving the promise that three() returns so the .then() handler on that promise never gets called.
I'm trying to build a complete theoretical mental model of how javascript handles concurrency. One of the missing pieces in that model is the relationship between network requests, promises, and the event loop. Take the above code snippet, and suppose I replace three's setTimeout with some sort of network request (a very common thing in async web development). Assuming that the network request behaves similarly to setTimeout, in that when the "worker thread" is done, a callback is pushed to the macro task queue, it's hard for me to understand how that callback even gets executed. But this is something that happens literally all the time.
Network requests and promises and timers all go through the event loop. There are very complicated rules about how multiple events in queue at the same time are prioritized relative to one another. .then() handlers are generally prioritized first.
Think of the Javascript interpreter as this simplistic sequence.
Get event from event queue
If nothing in the event queue, sleep until something is in the event queue
Run callback function associated with the event you pull from the event queue
Run that callback function until it returns
Note, it may not be completely done with its work because it may
have started other asynchronous operations and set up its own
callbacks or promises for those. But, it has returned from the
original callback that started it
When that callback returns, go back to the first step above and get the next event
Remember that network requests, promises, timers and literally ALL asynchronous operations in node.js go through the event queue in this manner.
Why you assume that network request would behave as setTimeout?
It is a Promise which resolved() would go in microtasks

What does microtask runs after every callback mean?

I'm reading this article on microtasks and there is the following conclusion:
In summary:
Tasks execute in order, and the browser may render between them
Microtasks execute in order, and are executed:
after every callback,
as long as no other JavaScript is mid-execution at the end of each
task
As I understand each task represents a single VM turn, so as long as no other JavaScript is mid-execution at the end of each task means when call stack is empty. But I don't understand what after every callback mean?
Can anyone please explain and show an example?
Here is the clear example that demonstrates this:
function cb1() {
console.log('cb1');
Promise.resolve('df').then(function promiseMicrotask() {
console.log('promise');
});
}
function cb2() {
console.log('cb2');
}
const element = document.querySelector('div.inner');
element.addEventListener('click', cb1);
element.addEventListener('click', cb2);
In the example above when you click on the div.inner, a browser schedules a task to handle the event and call the callbacks cb1 and cb2. Later it starts executing the task and triggers cb1. Inside cb1 a resolved promise schedules a microtask to run the promiseMicrotask callback. Whenever the current stack that started with cb1 is empty the browser checks the microtaks queue and finds a microtask promiseMicrotask. It triggers it and so it logs promise. Then it proceeds to triggering cb2.
So the microtask promiseMicrotask was processed after the callback cb1 but before cb2 and before the browser finished executing all callbacks in the current task.
What's important here is that all event callbacks are executed in current task.

Categories