What does the callstack look like with promises and async/await? - javascript

I recently watched this video: https://www.youtube.com/watch?v=8aGhZQkoFbQ and am trying to find a similar explanation for with Promises. Specifically in node.
I'm probably wrong but I'm under the impression that Promises are placed on a microtask queue, the microtask queue is only processed when the callstack is empty and once the event-loop starts processing the microtask queue, it processes it until the queue is empty.
const sleep = (ms) => new Promise((res) => {
setTimeout(res, ms)
});
const doTask1 = (workerId) => sleep(1000).then(() => console.log(`[${workerId}] Completed Task 1`));
const doTask2 = (workerId) => sleep(5000).then(() => console.log(`[${workerId}] Completed Task 2`));
const worker = async (workerId) => {
console.log(`[${workerId}] Starting work`)
await doTask1(workerId);
await doTask2(workerId);
console.log(`[${workerId}] Completed all tasks`);
};
worker('A');
worker('B');
When the snippet above is run, both worker A and B will start Task 1 and 2 at around the same time and complete at around the same time. I'm having trouble consolidating that behavior with what I've read/seen about Promises/microtasks and event-loops.
I imagine that when the code executes, the call stack looks like:
Callstack: [main.js]
Callstack: [main.js][worker('A')]
Callstack: [main.js][worker('A')][console.log()]
Callstack: [main.js][worker('A')][await doTask1()]
When we use await, what happens to the callstack? I suppose this is related to how generators work?
Callstack: ?? | Microtask Queue: [doTask1()]
What does it mean for the event-loop to "process" the microtask queue? Does processing doTask1() mean the event-loop is waiting 1 second? That doesn't sound right. Is the 1 second wait being done on a different thread (is that the right term?) as shown in the Youtube video's depiction of WebAPIs? Once the timeout is complete, the Promise's res goes onto the message queue where the event loop eventually picks it up and resolves the Promise? Does the callstack have to be empty for the callstack to execute res?
I'm stuck at this point and not sure what happens next in the callstack, the message queue, the microtask queue and how the event-loop decides what to do next.

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')

JavaScript async callbacks - Promise and setTimeout [duplicate]

This question already has answers here:
Promise vs setTimeout
(6 answers)
What is the relationship between event loop and Promise [duplicate]
(2 answers)
Closed 1 year ago.
In the following code:
setTimeout(() => console.log("hello"), 0);
Promise.resolve('Success!')
.then(console.log)
What should happen in my understanding:
setTimeout is called => print hello directly added to callback queue as time is 0
Promise.resolve => print Success! added to callback queue
If I am not wrong, the callback queue is FIFO.
But the code output is:
Success!
hello
What is the explanation?
There are 2 separate queues for handling of the callbacks. A macro and a micro queue. setTimeout enqueues an item in the macro queue, while promise resolution - to the micro queue. The currently executing macro task(the main script itself in this case) is executed synchronously, line by line until it is finished. The moment it is finished, the loop executes everything queued in the microtask queue before continuing with the next item from the macro queue(which in your case is the console.log("hello") queued from the setTimeout).
Basically, the flow looks like this:
Script starts executing.
MacrotaskQueue: [], MicrotaskQueue: [].
setTimeout(() => console.log("hello"), 0); is encountered which leads to pushing a new item in the macrotask queue.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].
Promise.resolve('Success!').then(console.log) is read. Promise resolves to Success! immediately and console.log callback gets enqueued to the microtask queue.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [console.log('Success!')].
The script finishes executing so it checks if there is something in the microtask queue before proceeding with the next task from the macro queue.
console.log('Success!') is pulled from the microtask queue and executed.
MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].
Script checks again if there is something else in the microtask queue. There is none, so it fetches the first available task from the macrotask queue and executes it, namely - console.log("hello").
MacrotaskQueue: [], MicrotaskQueue: [].
After the script finishes executing the console.log("hello"), it once again checks if there is anything in the microtask queue. It is empty, so it checks the macrotask queue. It is empty as well so everything queued is executed and the script finishes.
This is a simplified explanation, though, as it can get trickier. The microtask queue normally handles mainly promise callbacks, but you can enqueue code on it yourself. The newly added items in the microtask queue will still be executed before the next macrotask item. Also, microtasks can enqueue other microtasks, which can lead to an endless loop of processing microtasks.
Some useful reference resources:
The event loop
Microtasks
Using Microtasks
There are two different queues involved here: a Task queue and a Microtask queue.
Callback functions scheduled using setTimeout are added in the task queue whereas the callbacks scheduled using promises are added in the microtask queue or a job queue.
A microtask queue is processed:
after each callback as long as the call-stack is empty.
after each task.
Also note that if a microtask in a microtask queue queues another microtask, that will also be processed before processing anything in the task queue. In other words, microtask queue will be processed until its empty before processing the next task in the task queue.
The following code snippet shows an example:
setTimeout(() => console.log('hello'), 0);
Promise.resolve('first microtask')
.then(res => {
console.log(res);
return 'second microtask';
})
.then(console.log);
In your code, callback function of setTimeout is added to the task queue and the Promise.resolve queues a micro-task in a microtask queue. This queue is processed at the end of the script execution. That is why "success" is logged before "hello".
The following image shows a step-by-step execution of your code:
Resources for further reading:
Tasks, microtasks, queues and schedules
JavaScript job queue and microtasks
Even though the timeout is 0, the callback function will still be added to the web API (after being fetched from the call stack). Web APIs are threads that you can’t access; you can just make calls like Ajax, Timeout, and the DOM.
Promise.resolve schedules a microtask whereas setTimeout schedules a macrotask. Microtasks are executed before running the next macrotask.
So in your example, the
Promise.resolve('Success!').then(console.log);
will be executed before the setTimout since promises have better priority than the setTimeout callback function in the event loop stack.

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

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.

setTimeout performing before a task in micro task queue (Job queue)

I'm having trouble understanding why my code below logs the following in order:
"end"
"timeout done"
"promise"
I assumed that "promise" would log before "timeout done" since it has a priority over callback queue task (setTimeout).
My assumption after this observation is that until .then is called, the promise does not enqueue its task since it's not ready yet, and thus allows setTimeout to perform in the callback queue to be performed first. Is this correct?
const sampleFunction = function(e) {
setTimeout(() => console.log('timeout done'), 0)
const data = fetch(`https://jsonplaceholder.typicode.com/comments/1`)
.then(response => {
return response.json();
})
.then(json => {
/*doSomething*/
console.log('promise')
});
console.log('end')
}
Your console.log('promise') has to wait until your fetch().then() is done which involves a networking operation to another host (and thus non-zero time).
Your setTimeout() only has to wait until sampleFunction() returns and you get back to the event queue. So, since the fetch().then() is non-blocking and takes non-zero amount of time, when you first get back to the event queue, only the setTimeout() is ready to go. The networking operation will still be in process in the background.
So, this isn't a matter of prioritization. It's a matter of completion order. The setTimeout() inserts its completion event into the event queue long before the fetch() promise is resolved.
Perhaps you didn't realize that fetch() is non-blocking and calling it just initiates the operation and then your code execution continues after that?
In the console, you should see:
end
timeout done
promise
As long as your fetch() doesn't have any errors.

Difference between microtask and macrotask within an event loop context

I've just finished reading the Promises/A+ specification and stumbled upon the terms microtask and macrotask: see http://promisesaplus.com/#notes
I've never heard of these terms before, and now I'm curious what the difference could be?
I've already tried to find some information on the web, but all I've found is this post from the w3.org Archives (which does not explain the difference to me): http://lists.w3.org/Archives/Public/public-nextweb/2013Jul/0018.html
Additionally, I've found an npm module called "macrotask": https://www.npmjs.org/package/macrotask
Again, it is not clarified what the difference exactly is.
All I know is, that it has something to do with the event loop, as described in https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
and https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
I know I should theoretically be able to extract the differences myself, given this WHATWG specification. But I'm sure that others could benefit as well from a short explanation given by an expert.
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. While these microtasks are processed, they can queue even more microtasks, which will all be run one by one, until the microtask queue is exhausted.
What are the practical consequences of this?
If a microtask recursively queues other microtasks, it might take a long time until the next macrotask is processed. This means, you could end up with a blocked UI, or some finished I/O idling in your application.
However, at least concerning Node.js's process.nextTick function (which queues microtasks), there is an inbuilt protection against such blocking by means of process.maxTickDepth. This value is set to a default of 1000, cutting down further processing of microtasks after this limit is reached which allows the next macrotask to be processed)
So when to use what?
Basically, use microtasks when you need to do stuff asynchronously in a synchronous way (i.e. when you would say perform this (micro-)task in the most immediate future).
Otherwise, stick to macrotasks.
Examples
macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
microtasks: process.nextTick, Promises, queueMicrotask, MutationObserver
Basic concepts in spec:
An event loop has one or more task queues.(task queue is macrotask queue)
Each event loop has a microtask queue.
task queue = macrotask queue != microtask queue
a task may be pushed into macrotask queue,or microtask queue
when a task is pushed into a queue(micro/macro),we mean preparing work is finished,so the task can be executed now.
And the event loop process model is as follows:
when call stack is empty,do the steps-
select the oldest task(task A) in task queues
if task A is null(means task queues is empty),jump to step 6
set "currently running task" to "task A"
run "task A"(means run the callback function)
set "currently running task" to null,remove "task A"
perform microtask queue
(a).select the oldest task(task x) in microtask queue
(b).if task x is null(means microtask queues is empty),jump to step (g)
(c).set "currently running task" to "task x"
(d).run "task x"
(e).set "currently running task" to null,remove "task x"
(f).select next oldest task in microtask queue,jump to step(b)
(g).finish microtask queue;
jump to step 1.
a simplified process model is as follows:
run the oldest task in macrotask queue,then remove it.
run all available tasks in microtask queue,then remove them.
next round:run next task in macrotask queue(jump step 2)
something to remember:
when a task (in macrotask queue) is running,new events may be registered.So new tasks may be created.Below are two new created tasks:
promiseA.then()'s callback is a task
promiseA is resolved/rejected: the task will be pushed into microtask queue in current round of event loop.
promiseA is pending: the task will be pushed into microtask queue in the future round of event loop(may be next round)
setTimeout(callback,n)'s callback is a task,and will be pushed into macrotask queue,even n is 0;
task in microtask queue will be run in the current round,while task in macrotask queue has to wait for next round of event loop.
we all know callback of "click","scroll","ajax","setTimeout"... are tasks,however we should also remember js codes as a whole in script tag is a task(a macrotask) too.
I think we can't discuss event loop in separation from the stack, so:
JS has three "stacks":
standard stack for all synchronous calls (one function calls another, etc)
microtask queue (or job queue or microtask stack) for all async operations with higher priority (process.nextTick, Promises, Object.observe, MutationObserver)
macrotask queue (or event queue, task queue, macrotask queue) for all async operations with lower priority (setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering)
|=======|
| macro |
| [...] |
| |
|=======|
| micro |
| [...] |
| |
|=======|
| stack |
| [...] |
| |
|=======|
And event loop works this way:
execute everything from bottom to top from the stack, and ONLY when the stack is empty, check what is going on in queues above
check micro stack and execute everything there (if required) with help of stack, one micro-task after another until the microtask queue is empty or don't require any execution and ONLY then check the macro stack
check macro stack and execute everything there (if required) with help of the stack
Micro stack won't be touched if the stack isn't empty. The macro stack won't be touched if the micro stack isn't empty OR does not require any execution.
To sum up: microtask queue is almost the same as macrotask queue but those tasks (process.nextTick, Promises, Object.observe, MutationObserver) have higher priority than macrotasks.
Micro is like macro but with higher priority.
Here you have "ultimate" code for understanding everything.
console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);
const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
setTimeout(() => {
console.log('stack [4]')
setTimeout(() => console.log("macro [5]"), 0);
p.then(() => console.log('micro [6]'));
}, 0);
console.log("stack [7]");
});
console.log("macro [8]");
/* Result:
stack [1]
macro [8]
stack [7], stack [7], stack [7]
macro [2]
macro [3]
stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]
macro [5], macro [5], macro [5]
--------------------
but in node in versions < 11 (older versions) you will get something different
stack [1]
macro [8]
stack [7], stack [7], stack [7]
macro [2]
macro [3]
stack [4], stack [4], stack [4]
micro [6], micro [6], micro [6]
macro [5], macro [5], macro [5]
more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
*/
Macro tasks include keyboard events, mouse events, timer events (setTimeout) , network events, Html parsing, changing Urletc. A macro task represents some discrete and independent work. micro task queue has higher priority so macro task will wait for all the micro tasks are executed first.
Microtasks, are smaller tasks that update the application state and should be executed before the browser continues with other assignments such as
re-rendering the UI. Microtasks include promise callbacks and DOM mutation changes. Microtasks enable us to execute certain actions before the UI is re-rendered, thereby avoiding unnecessary UI rendering that might show an inconsistent application state.
Separation of macro and microtask enables the
event loop to prioritize types of tasks; for example, giving priority to performance-sensitive tasks.
In a single loop iteration, one macro task at most is processed
(others are left waiting in the queue), whereas all microtasks are processed.
Both task queues are placed outside the event loop, to indicate that the act of adding tasks to their matching queues happens outside the
event loop. Otherwise, any events that occur while JavaScript code is
being executed would be ignored. The acts of detecting and adding
tasks are done separately from the event loop.
Both types of tasks are executed one at a time. When a task starts executing, it’s executed to its completion. Only the browser can stop
the execution of a task; for example, if the task takes up too much
time or memory.
All microtasks should be executed before the next rendering because their goal is to update the application state before rendering occurs.
The browser usually tries to render the page 60 times per second,
It's accepted that 60 frames per second are the rate at which
animations will appear smooth. if we want to achieve smooth-running
applications, a single task, and all microtasks generated by that task
should ideally complete within 16 ms. If a task gets executed for more
than a couple of seconds, the browser shows an “Unresponsive script”
message.
reference John Resig-secrets of JS Ninja
I created an event loop pseudocode following the 4 concepts:
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering are part of the macrotask queue. One macrotask item will be processed first.
process.nextTick, Promises, Object.observe, MutationObserver are part of the microtasks queue. The event loop will process all of the items in that queue including once that were processed during the current iteration.
There is another queue called animation queue which holds animation changes task items which will be processed next. All tasks which exists in this queue will be processed (not including new one which were added during the current iteration). It will be called if it is time for rendering
The rendering pipeline will try to render 60 times a second (every 16 ms)
while (true){
// 1. Get one macrotask (oldest) task item
task = macroTaskQueue.pop();
execute(task);
// 2. Go and execute microtasks while they have items in their queue (including those which were added during this iteration)
while (microtaskQueue.hasTasks()){
const microTask = microtaskQueue.pop();
execute(microTask);
}
// 3. If 16ms have elapsed since last time this condition was true
if (isPaintTime()){
// 4. Go and execute animationTasks while they have items in their queue (not including those which were added during this iteration)
const animationTasks = animationQueue.getTasks();
for (task in animationTasks){
execute(task);
}
repaint(); // render the page changes (via the render pipeline)
}
}

Categories