6

Which of a generator's yield vs. promise.then() is a more* correct mental model for understanding 'await'?

Property comparison, inferred by stepping through the snippet below with a debugger:

await:

  1. await does not pause/suspend the running async function’s execution. (The running async function ‘runs to completion’, returning a pending promise when the interpreter hits the 1st await. It’s then immediately removed from the call stack.)

  2. await waits for the promise to settle.

  3. await expression wraps the rest of a function's code in a microtask.

generator-yield:

  1. yield pauses the running function’s execution. generator functions do not ‘run to completion’.
  2. yield promise does ensure promise has settled prior to executing remaining code.
  3. yield does not wrap or create a microtask.

promise.then(callback):

  1. does not pause the running function’s execution.
  2. waits for promise to settle before executing callback.
  3. creates a microtask (callback)

//promise returning function
function foo(whoCalled) {
   let p = new Promise(function(resolve, reject) { 
     setTimeout( () => {
       console.log('resolving from setTimeout - called by: ' + whoCalled)
       resolve('resolve value') }, .1)
   })
   return p
}

//async await
async function asyncFunc() {
  await foo('async function')
  //rest of running function’s code…
  console.log('async function howdy')
}

//generator yield:
function* gen() {
   yield foo('generator function')
   //rest of running function’s code…
   console.log('generator function howdy')
}

//promise.then():
function thenFunc() {
   let r = foo('promise.then function').then(() => {
       //rest of running function’s code…
       console.log('promise.then() howdy')
   })
   return r
}

//main
function main() {

  //async await
  var a = asyncFunc() 
  console.log(a) //logs Promise { <pending> }
                 //the rest of the code following await foo() runs as a microtask runs once foo() resolves. The call stack was cleared.

  //generator
   var g = gen()
   console.log(g) // logs Object [Generator] {}
   var p = g.next().value
   console.log(p) //logs Promise { <pending> }
   g.next()       //the rest of the code following yield running gen function's code runs. call stack was not cleared.

   //promise.then()
   var x = thenFunc()
   console.log(x) //logs Promise { <pending> }
                   //the then(callback) microtask runs once foo() resolves. The call stack was cleared
}
main()
console.log('main is off the call stack - launch/startup macrotask completing. Event loop entering timer phase.')

And, going beyond this comparison, what is the accurate mental model of what await does under the hood?

await in latest ECMAScript spec for reference: https://www.ecma-international.org/ecma-262/10.0/index.html#await

await in V8 source code: https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/builtins/builtins-async-function-gen.cc#L252

AnonEq
  • 267
  • 2
  • 9
  • 1
    `await` is like `.then()` – zer00ne Jul 04 '19 at 16:51
  • "*await does not pause/suspend the execution.*" and "*when the interpreter hits the 1st await. It’s then immediately removed from the call stack.*" do seem contradictory to me. What do you mean by that paragraph? – Bergi Jul 04 '19 at 18:13
  • https://babeljs.io/repl can be helpful in figuring this out. Copy paste your code there and see how it simulates the await keyword. – Todd Chaffee Jul 04 '19 at 19:18
  • @Bergi 1st quote = describes a pause in a function’s code for later resumption (not running to completion), vs. 2nd quote = describes wrapping the remainder of a function's code in a microtask, hitting the end curly brace and returning (run to completion). – AnonEq Jul 05 '19 at 14:12
  • @ToddChaffee Good tip. Is it correct to say the following? Transpiling can produce functionally equivalent vanilla Javascript. And while this may be helpful for inferring what's going on 'under the hood', it could be misleading? (for example: transpiled async await may not map to the ECMAScript async await spec) – AnonEq Jul 05 '19 at 15:18

3 Answers3

5

It's not one or the other. Actually it's both of them together: async/await = yield + then + a runner.

An async function does get suspended by the await keyword just like a generator function* does get suspended by the yield keyword. The mechanism of how the execution gets stopped and resumed in the middle of control flow statements is exactly the same.

What differs is how these continuations are driven, and what the functions return. A generator function creates a generator object when called, and you have to explicitly invoke the next() method from outside to run the code yield by yield. An async function on the other hand creates a promise, and manages the execution by itself. It doesn't wait for external next() calls but runs each asynchronous step as soon as possible. Instead of returning the yielded values from those next() calls, it does Promise.resolve() the awaited values to a promise, and calls its then method passing the continuation as the callbacks. Instead of signalling an "end of iteration" to the caller when reaching a return, it resolves the originally returned promise with the return value.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Hi Bergi, can you say more about the nature of "as soon as possible". `Async`/`await` so successfuly mimics synchronous code, there's a real danger that people misunderstand the non-blocking nature of an asyncFunction with await(s). – Roamer-1888 Jul 04 '19 at 22:26
  • 1
    @Roamer-1888 I've made it clear that the execution is still async. – Bergi Jul 05 '19 at 09:22
  • @Bergi Thank you. Is [v8 await source code](https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/builtins/builtins-async-function-gen.cc#L243) the kind of thing you reference to determine this? Or ECMASpec abstract operations comparison [async function](https://tc39.es/ecma262/#sec-async-functions-abstract-operations) [generator](https://tc39.es/ecma262/#sec-generator-abstract-operations). Also, not sure what you mean by a 'runner' here? – AnonEq Jul 05 '19 at 16:37
  • @Bergi Regarding: "...and calls its `then` method passing the continuation as the callbacks." I can't seem find where the `.then(continuation code callback)` is implemented in v8 await src code. – AnonEq Jul 05 '19 at 16:54
  • 1
    @AnonEq I learn from the spec about behaviour and from the V8 blog about optimisations. I don't read the code. The "runner" refers to the abstract idea of an entity that drives (runs) the code. In transpiled code, there e.g. is a function that takes care of dealing with the awaited promises and keeping things going by installing the then callbacks. – Bergi Jul 06 '19 at 09:36
2

Promises and yield are not the easiest to grasp, especially not when you don't know how they work under the hood. So let's start with the basics. The first thing to understand is that Javascript is single threaded, which means that it can only do one thing at the same time. The way you are still able to multiple things at 'once' is because javascript has a thing called an event loop.

The event loop is basically looks something like this:

while(queue.waitForTasks()) {
   queue.performNextTask();
}

What the event loop does is check if there are new 'tasks' for Javascript to run. If there is a task. then it gets executed until there are no more tasks left to execute. And it will wait for its new task. These tasks are stored in something that is called a queue.

Promises, Async/Await

Now we understand how Javascript processes the different tasks. How does it work with promises, and async/await? A promise is nothing more than a task, or in the case of Javascript something that holds a task, that will be added to the queue and executed once all tasks before it have been executed. The .then() is a way of providing a callback to your promise that gets executed once your resolve callback is called.

the await [something] keyword tells Javascript, hey put the next [something] on the end of your queue, and get back to me once that [something] has a result to give.

A function that has the async keyword is basically telling Javascript: 'This function is a promise, but execute it immediately'.

The flow of a async function is easiest to grasp/demonstrate with two different async functions A and B like this:

const A = async () => {
    console.log(A: Start);
    for (var i = 0; i < 3; i++) {
        await (async () => console.log('A: ' + i));
    }
    console.log('A: Done');
}
const B = async () {
    console.log(B: Start);
    for (var i = 0; i < 3; i++) {
        await (async () => console.log('B: ' + i));
        await (async () => {/* A task without output */});
    }
    console.log('B: Done');
}

When you are calling your functions with await like this:

console.log('Executing A');
await A();
console.log('Executing B');
await B();

it would result in:

Executing A
A: Start
A: 0
A: 1
A: 2
A: Done
Executing B
B: Start
B: 0
B: 1
B: 2
B: Done

and running:

console.log('Executing A');
A();
console.log('Executing B');
B();

would result in:

Executing A
A: Start       Note: still gets ran before Executing B
Executing B
B: Start
A: 0
B: 0
A: 1
A: 2           Note: A: 2 first because another task in B was put in the queue
A: Done
B: 1
B: 2
B: Done

Understanding this might help to better understand the flow of your application.

yield

The yield keyword is similar to await in the sense that an 'outside force' controls when it continues the flow of the function. In this case not the completion of the promise task, but the generator.next() function

Software Person
  • 2,526
  • 13
  • 18
  • 1
    A promise is not a task, and it does not get put on the event queue. It's the task of executing the `then` callback that gets put on the queue *when the promise fulfills*. – Bergi Jul 04 '19 at 20:15
  • 1
    Okay maybe I should change my phrasing a bit, since it is not very exact. As you might have noticed. I'll do that later though, this already took a while to write haha – Software Person Jul 04 '19 at 20:19
  • 1
    @SoftwarePerson "A promise is nothing more than a task." Incorrect. Might instead say 'a promise is nothing more than an object representing an eventual return/resolve value'. "await [something] keyword tells Javascript, hey put the next [something] on the end of your queue, and get back to me once that [something] has a result to give." Incorrect. [something] is executed immediately, and synchronously. It's subsequent function code that is wrapped in a microtask. – AnonEq Jul 05 '19 at 12:29
  • @SoftwarePerson "A function that has the async keyword is basically telling Javascript: 'This function is a promise, but execute it immediately'." Incorrect. The async keyword is interpreted as, 'this function returns a promise, and await can be used in the func body'. – AnonEq Jul 05 '19 at 12:29
  • 1
    The code doesn't generate that result. By `await (async () => console.log('A: ' + i))` you're awaiting on *definition* of anonymous function which is indeed a non-thenable object. You need to execute the anonymous function: `await ((async () => console.log('A: ' + i))())` – Alireza Feb 19 '20 at 05:23
0

I don't know the answer to the correct mental model here, though I would really like to know.

But I found this interesting

Kyle Simpson author of 'You Don't Know JS' chimed in on how await works on r/Javascript reddit - source:

"This is entirely incorrect. Generators do not run-to-completion, and most engine implementations of async-await actually treat them like generators. When a yield is encountered, the generator is locally paused... literally. Await uses the same approach."

"No, this is all incorrect nonsense. Most engines treat async-await like a generator, which definitely does localy pause at a yield. Wrapping promise.then() around subsequent code would be one of the most naive and inefficient ways to implement await. Even if an engine did that (most don't) that doesn't mean that's the proper mental. model. Local pausing like yield is the proper mental model."

But when I personally look at the ECMA Script spec myself and walk though code with the vscode nodejs debugger, await seems much more analogous to .then()

Ask P
  • 355
  • 2
  • 14