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:
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.)
await waits for the promise to settle.
await expression
wraps the rest of a function's code in a microtask.
generator-yield:
yield promise
does ensure promise
has settled prior to executing remaining code.promise.then(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
A generator function is executed yield by yield i.e one yield-expression at a time by its iterator (the next method) whereas async-await, they are executed sequential await by await. Async/await makes it easier to implement a particular use case of Generators.
Promise is an object representing intermediate state of operation which is guaranteed to complete its execution at some point in future. Async/Await is a syntactic sugar for promises, a wrapper making the code execute more synchronously.
functionality: yield and await can both be used to write asynchronous code that “waits”, which means code that looks as if it was synchronous, even though it really is asynchronous. await: This is an operator which used to wait for a Promise.
Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished. Once that task has finished, your program is presented with the result.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With