The big question for me is how this relates to the iterator protocol, because iterators are one of the most important applications for code that runs sometimes-synchronously. A sometimes-synchronous iterator is what I would call a "stream".
Streams are a great area to push for progress in because they make data access effortless, especially text. What's brilliant about thinking of text as a character stream is that a string is a character stream, and so is open file handle. You can implement algorithms like decodeUTF8 in a way that's completely agnostic to whether you have the source byte in an array or a file on disk. That's great! That's what a successful abstraction looks like.
In practice it lets API design be simplified. Right now you read files with read(file, encoding). This is technically a mixing of unrelated concerns: one concern is reading the bytes, the other is encoding them. The concerns are mashed together because we lack(ed) the tools to pry them apart. Now the API can be: decodeUTF8(read(file)). I have implemented this here: node-fs/lib/index.js at 6dabc6916082eb09cbeb61ee0d389056b401b6cb · bablr-lang/node-fs · GitHub
This is highly relevant because this proposal implies that all async iterators would become possible stream iterators. Iteration is a protocol which means you don't control how the provider provides the protocol to you. If promises are synchronously inspectable, any defensively minded programmer must consider the possibility that an async iterator contains some synchronously available values. If the async iterator is a file stream, the vast majority of values (bytes) will be available synchronously.
For example:
let iter = thing[Symbol.asyncIterator]();
let step = iter.next();
if (!step.fulfilled) await step;
console.log(step.value); // synchronous inspection
Since the low-level protocol itself suggest the possibility of efficient stream processing by not waiting where waiting is unneeded, I think it only makes sense for the language to also make the same things efficient using high-level protocols.
I have proposed and still recommend: for await? loops as a high-level construct that behaves the way the above example code does, waiting only when the stream requires waiting.
for await? (let chr of textStream) {
// ...
}
If you can consume this type of stream you're going to want to be able to produce it, and that implies that generator functions are allowed to return sync-inspectable promises from next() under certain conditions.
I still think my proposal here is the most comprehensive way of dealing with all of this: for await? .. of - #19 by conartist6
It still creates an explicit third protocol, but when coercing async iterators to the stream iterator protocol, if both proposals were accepted then we would respect the sync-ness of steps in the async iterator when possible