So after some introspection and exploration, the answer is somewhat obvious.
When using generators it is the application developer's responsibility to pass the generator to the runner (eg. co). Async/await can be thought of as a higher level abstraction than can build upon this to make this implicit and remove the responsibility from application developer's hands.
Whenever we await inside an async function, the runtime ensures that we either get back a resolved value or fail with an exception.
Plus, similar to async function always return a promise irrespectively what the code returns, await can handle things that are not promises (by effectively calling Promise.resolve).
This makes async await more obvious choice for asynchronous control flow and generators for their intended use case - building iterators. This intent is also supported by the upcoming TC39 async-iteration proposal (with a regenerator polyfill) that further extends these ideas to support asynchronous iterators:
async function* iterator(arg) { .... }
Async generator functions are similar to generator functions, with the following differences:
- When called, async generator functions return an object, an async generator whose methods (next, throw, and return) return promises for { next, done }, instead of directly returning { next, done }. This automatically makes the returned async generator objects async iterators.
- await expressions and for-await-of statements are allowed.
- The behavior of yield* is modified to support delegation to async iterables.
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
This makes the usage scenarios explicit and makes interoperability seamless.