2

In Javascript, the callback from queue is run only when the call stack is empty.

I understand that when the program finishes execution, the call stack becomes empty. This is the time the callback will run.

This question is about await. Suppose the main program (by main I am referring to the outer most program code) calls a function that calls fetch async and awaits for response. Until the response arrives, the control goes to the caller (main program). Now suppose there is an await in the main program, then does this make the call stack empty and thus run the callback without the program finishing execution?

variable
  • 8,262
  • 9
  • 95
  • 215
  • await halts that "thread" or function until the value that is being waited on is provided from the promise. it does not empty the call stack. it basically turns a non-blocking Promise into a blocking call, but only within an asynchronous context. (an async function) – David Culbreth Aug 09 '21 at 14:47
  • No. Await works exactly like a callback. That is, `await` returns immediately allowing other code to run. All code after the `await` is compiled into a callback and will be called when the promise completes. Exactly the same as if you manually copy/pasted the code after `await` into a `.then()` callback – slebetman Aug 09 '21 at 14:58
  • Or to put it another way, `await` does not **make** the call stack empty, it **waits** for it to be empty (AND for the promise to resolve) – slebetman Aug 09 '21 at 15:03
  • I have modified the question to make my intent more clear. – variable Aug 09 '21 at 15:12
  • "*Now suppose there is an await in the main program*" - are you referring to top-level await? – Bergi Aug 09 '21 at 15:24
  • Yes top level. My main program I mean top level – variable Aug 09 '21 at 15:33

1 Answers1

0

No, the await keyword does not empty the call stack. When the operator is evaluated, it just pops of the top stack frame, suspending the execution of the async function code that was running, but that's it. The rest of the call stack stays intact. Consider

async function inner() {
    await new Promise(() => {});
}
function outer() {
    inner();
}
outer();

When the await is encountered, it removes inner from the call stack, but outer still stays on the call stack, and the lines following the inner(); statement will be executed. The program then has to "finish" (run all the way to the end of the script) before an asynchronous callback can resolve the promise, and resume the suspended execution of the async function.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I have modified the question to make my intent more clear. Request you to reconsider. – variable Aug 09 '21 at 15:12
  • This is a good explanation but would be stronger still if a) you provided a reference that documents this behaviour and b) you could explain what happens when the program has then finished (the call stack is empty), the event loop fetches the next callback from its queue etc ... where the `inner()` function and it's state, ready for resuming, reside. The reason I ask is that you said `inner()` was popped off the stack and so it's not there. Which leaves the possibility that it is, state conserved pushed onto the end of the event loop queue? But that is not the only possibility. – Bernd Wechner Aug 26 '21 at 06:54
  • @BerndWechner Sorry I don't have any references to articles at hand and didn't want to link the spec. "*Where does the `inner()` function and it's state, ready for resuming, reside?*" - the suspended function call state is stored somewhere in heap, very much like the variables of a closure. It's not pushed to the end of the event loop yet, a function that resumes it is registered as a handler of the promise that is `await`ed (through a `.then(…)` call). – Bergi Aug 26 '21 at 07:01
  • Ah, I see. So to clarify: When the promise is instantiated with an executor, the executor is given a function to call on resolution (and one to call when failing). The executor is immediately executed (at instantiation, and calling the resolver simply sets the state of the Promise to "fulfilled". `await` then merely checks the state but if "pending" pauses. When `inner()` is popped from the stack and `outer()` is till on it, what is running? – Bernd Wechner Aug 26 '21 at 07:09
  • @BerndWechner `await` doesn't actually check the state, it *always* pauses the function and calls `.then()`. This will check the state, scheduling the handler if it's already fulfilled, or register the handler with the promise so that it gets scheduled when the promise is fulfilled later. "*When `inner()` is popped from the stack and `outer()` is still on it, what is running?*" - then `outer` would be at the top of the stack. (In the code example in my answer, it doesn't actually do anything though but immediately returns). – Bergi Aug 26 '21 at 07:13
  • Getting closer, thanks heaps. What handler does await pass to `.then()`? And what does scheduling mean here? While it is paused on await, why then is `inner()` not top of the stack? If it `outer()` now running because it does not `await` inner, I guess `outer` continues. But what if there was code in `inner` after the await. How and when does that run? I'm going to guess the default `.then()` handler that await registers calls back to `inner` (as in if await was a yield, then the default `.then()` handler calls `.next()` to resume `inner`. – Bernd Wechner Aug 26 '21 at 07:21
  • But to be honest, linking to the spec and the right section is not a bad idea for those who want to drill down. – Bernd Wechner Aug 26 '21 at 07:36
  • I think I'm getting closer. https://javascript.info/microtask-queue. await does indeed queue a handler to call back (the equivalent of the `.next()` call on an iterator, but on a different queue currently dedicated for promises. It has a higher priority: https://javascript.info/event-loop#summary, but is still handled by the event loop. So await does push a task onto the event queue, with a caveat, on the microtasks (inner) queue, not the macrotasks (outer) queue. Comprehension is approaching ... – Bernd Wechner Aug 26 '21 at 07:52
  • "*I'm going to guess the `then` handler that `await` registers calls back to inner, as if in `await` was a `yield`*" - yes, *exactly* like that. The mechanism of suspending the running function is the same - it's just that a generator requires a `.next()` call from outside, while `await` registers that itself on the promise. – Bergi Aug 26 '21 at 07:55
  • "*So `await` does push a task onto the microtask queue*" - I wouldn't say that. It's the promise that does this. And it only happens when the promise is getting fulfilled, which generally happens way after the `await` was executed. – Bergi Aug 26 '21 at 07:57
  • Getting closer still! So the Promise, when instantiated, puts the executor (the argument passed to the constructor) onto the end of the microtask queue (with two callbacks as arguments). These two callbacks by default, set the state to fulfilled and rejected respectively and then call any handlers registered by `.then()`? – Bernd Wechner Aug 26 '21 at 10:51
  • The stack under `await` and the manner in which it pauses remains mysterious. `await` yields and registers a callback (equivalent of a generators `.next()`) with the promise, to call when the executor calls its first argument. By yielding it effectively returns, to `outer` in your example. This would imply that the executor may never be called (if `outer` for example never ends (has for example a `while (true) {}` after the call to `inner()`. Because `outer()` never ends the stack never empties and the event loop never pulls off the microtask queue? – Bernd Wechner Aug 26 '21 at 10:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236438/discussion-between-bernd-wechner-and-bergi). – Bernd Wechner Aug 26 '21 at 10:56
  • @BerndWechner you should separate the promise creation from the `await` operator. `await` really is just some syntactic sugar for `.then()` calls. [`new Promise` does call the executor synchronously](https://stackoverflow.com/q/29963129/1048572), only [for passing `resolve`/`reject` and error handling](https://stackoverflow.com/q/37651780/1048572) not to make something asynchronous. Inside the executor, you'd start an asynchronous task (e.g. `setTimeout`), and have it call `resolve` at the end. The `resolve` call schedules the previously registered handlers on the microtask queue. – Bergi Aug 26 '21 at 12:41
  • @BerndWechner All this promise instantiation (including the executor execution) happens before the promise is passed to `await`. But yes, if `outer` hogs the cpu, `inner` won't be able to advance from the `await` since the task that resolves the promise, or the microtask that resumes the execution of `inner`, is blocked from being de-queued. – Bergi Aug 26 '21 at 13:49
  • `.then()` I presume pushes an internal Promise method onto the microtask queue (call it CHECK_PROMISE). When CHECK_PROMISE runs it checks the status of the promise. If fulfilled it calls any handler `.then()` registered, and if rejected, likewise. If pending, what then? Presumably pushes CHECK_PROMISE back onto the microtask queue? But not. Can't do. For if it did, the microtask queue would never be empty until the Promise resolved, meaning no task on the macrotask queue ever runs, and thus can't resolve it (but it visible can, so something is missing). – Bernd Wechner Aug 27 '21 at 00:36
  • @BerndWechner `.then()` immediately checks the state of the promise. If it's settled already, it pushes a task that will call the respective callback and resolve the returned promise onto the microtask queue. If it's pending still, it appends that task to an internal list on the promise object, to be scheduled on the microtask queue by the `resolve()`/`reject()` call when the promise is settled later. – Bergi Aug 27 '21 at 09:15
  • Aha, I think we've nailed it at last. `.then()` (and `await`) check the state, and if "pending" register a callback request on an internal list in the Promise. Nothing gets scheduled on the microtask queue. Which solves the problem. Namely the microtask queue can now run to completion eventually empty then the macrotask view gets a look in. In that was a task on the macrotask view can call the resolver, whose job it is to change state to fulfilled and then place any registered callbacks on the microtask queue. Voila, and it is all possible and makes sense! Muchas gracias! – Bernd Wechner Aug 27 '21 at 10:36
  • Hi @Bergi EXCELLENT ANSWER. So, based, on the answer you gave, is the following that David Culbreth says true? `sawait halts that "thread" or function until the value that is being waited on is provided from the promise. it does not empty the call stack. it basically turns a non-blocking Promise into a blocking call, but only within an asynchronous context. (an async function)` thanks in advance –  Jan 24 '23 at 13:44
  • @Daniel More or less, yes. – Bergi Jan 24 '23 at 14:45