How does the Call Stack behave when async/await functions are used ?
function resolveAfter2Seconds() { // taken from
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
const asyncFuntion=async()=>{
const result = await resolveAfter2Seconds();
console.info("asyncFuntion finish, result is: ", result);
}
const first = async()=>{
await asyncFuntion();
console.log('first completed');
debugger;
}
const second = ()=>{
console.log('second completed');
debugger;
}
function main(){
first();
second();
}
main();
In the above code, when the first breakpoint is encountered in second(), I could see that the call stack contained main() and second(). And during the second breakpoint in first(), the call stack contained main() and first().
What happened to first() during the first breakpoint. Where is it pushed ? Assuming that the asyncFunction() takes some time to complete.
Someone please help.
How does the Call Stack behave when async/await functions are used ?
function resolveAfter2Seconds() { // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
const asyncFuntion=async()=>{
const result = await resolveAfter2Seconds();
console.info("asyncFuntion finish, result is: ", result);
}
const first = async()=>{
await asyncFuntion();
console.log('first completed');
debugger;
}
const second = ()=>{
console.log('second completed');
debugger;
}
function main(){
first();
second();
}
main();
In the above code, when the first breakpoint is encountered in second(), I could see that the call stack contained main() and second(). And during the second breakpoint in first(), the call stack contained main() and first().
What happened to first() during the first breakpoint. Where is it pushed ? Assuming that the asyncFunction() takes some time to complete.
Someone please help.
Share Improve this question edited Oct 14, 2020 at 18:25 Victor 3,9783 gold badges40 silver badges65 bronze badges asked May 21, 2019 at 15:05 Zeeshan ShamsuddeenZeeshan Shamsuddeen 6276 silver badges18 bronze badges 7 | Show 2 more comments1 Answer
Reset to default 21First off, when you get to the breakpoint you hit in second
, first
has already executed and is no longer on the stack.
When we go into first
, we instantly hit an await asyncFunction()
. This tells JavaScript to not block on the result of the call, but feel free to go looking for something else to do while we're waiting. What does Javascript do?
Well, first of all, it does call asyncFunction()
. This returns a promise, and will have started some asynchronous process that will later resolve it. Now we can't continue with the next line of first
(ie, the console.log('first completed')
) because our await
means we can't carry on until the promise was fulfilled, so we need to suspend execution here and go find something else to do with our free time.
So, we look up the stack. We're still in the first()
call from main, and now we can just return a promise from that call, which we will resolve when the asynchronous execution completes. main
ignores that promise return value, so we continue right with second()
. Once we've executed second
, we look back to whatever called that, and carry on with synchronous execution in the way everyone would expect.
Then, at some point in the future, our promise fulfills. Maybe it was waiting on an API call to return. Maybe it was waiting on the database to reply back to it. In our example, it was waiting for a 2s timeout. Whatever it was waiting for, it now is ready to be dealt with, and we can un-suspend the first
call and continue executing there. It's not "called" from main
- internally the function is picked back up, just like a callback, but crucially is called with a completely new stack, which will be destroyed once we're done calling the remainder of the function.
Given that we're in a new stack, and have long since left the 'main' stack frame, how do main
and first
end up on the stack again when we hit the breakpoint inside it?
For a long time, if you ran your code inside debuggers, the simple answer was that they wouldn't. You'd just get the function you were in, and the debugger would tell you it had been called from "asynchronous code", or something similar.
However, nowadays some debuggers can follow awaited code back to the promise that it is resolving (remember, await
and async
are mostly just syntactic sugar on top of promises). In other words, when your awaited code finishes, and the "promise" under the hood resolves, your debugger helpfully figures out what the stack "should" look like. What it shows doesn't actually bear much resemblance to how the engine ended up calling the function - after all, it was called out of the event loop. However, I think it's a helpful addition, enabling us all to keep the mental model of our code much simpler than what's actually going on!
Some further reading on how this works, which covers much more detail than I can here:
- Zero-cost async stack traces
- Asynchronous stack traces
synchronous
andasynchronous
functions together. I am not very sure tough, but I can imagine,main()
starts, and pushesfirst()
to callstackx
since it'sasync
. Immediately afterfirst()
was invoked,second()
is called and pushed to a different callstack, cuzsync
. I usually imagineasync
funcions to myself as some sort of threads. – Silvan Bregy Commented May 21, 2019 at 20:37nodes
has a singleeventloop
. But theeventloop
is just the final executioner. I think there'll still be differentcallstacks
or something like queues. In the end, the eventloop takes all these queues and just executes them in some order. I have not enough knowledge to answer this question correctly , so I just put this as comment. – Silvan Bregy Commented May 22, 2019 at 8:32callstack
. I think it's just the stacktrace. I guess this is handled more low-level.` – Silvan Bregy Commented May 22, 2019 at 8:42