Ecma262: Why the value property of the returned IteratorResult object shouldn't be a Promise (nor a thenable) in case of asynchronous iteration?

Created on 18 Aug 2019  ·  14Comments  ·  Source: tc39/ecma262

Each method's description of the AsyncIterator interface says that _the IteratorResult object that serves as a fulfilment value should have a value property whose value is not a promise (or "thenable")_.

I'd like to understand why.

The JS lang itself tries to avoid such a case: if we yield a __Promise__ from an _async generator_ it will be implicitly awaited, so we are not able to insert a promise inside the value field of the returned IteratorResult.
If instead we manually implement the async iteration interfaces to force the value to be a Promise, ES2018 async-iteration aware syntax is not going to help us: the for-await-of won't wait for us that promise.

But, the for-await-of is already able to handle __sync__ iterables that _yield_ promises as values. So it could have been designed with full support for __async__ iterables that _yield_ promises too. What about _async generators_? Without the "implicit" await, promises and thenables could be yielded.

Why were these choices made? Unfortunately, I have not found a valid answer so I'm asking here.
My personal opinion is that all this would have resembled too much with ​​a promise (the one returned by the __next__ method, for example) of promise (the one inside the value field). A concept from which the JavaScript lang always kept away.

Thanks in advance 😄

question

Most helpful comment

All the way back when promises were introduced into JS, TC39 consciously decided against making them a proper monad. So JS promises do not compose. Design mistakes like that are typically paid for later.

All 14 comments

My very vague recollection is that it's exactly that - that we can't have a Promise fulfilled with a Promise, and what you're asking about would be too close to it.

Hopefully @domenic or someone else can answer with more certainty.

if you put the promise in the value field, the done field can't accurately reflect the state of the iterator, since its now disconnected from whatever the source of the iterator is.

All the way back when promises were introduced into JS, TC39 consciously decided against making them a proper monad. So JS promises do not compose. Design mistakes like that are typically paid for later.

@devsnek Hoping not to say something heretic...

Considering the fact that async iteration helps us interfacing with external data sources, a great number of JS async sources are only a conduit for data. Could it happen that the resolution of the done field (although still asynchronous), given the great variety of external sources kinds, can be done without the need of knowing the actual requested value, from the async source perspective?

In other words, when interfacing with the external source of data, our async source could ever receive information about the value and its state (is it the last or not?) separately? In such a case, it should be able to asynchronously resolve the done flag for the consumer as soon as possible, but the not yet ready value would be a promise.

async generators are buffering, because generally the idea is that no matter when you call next, you should get a temporally consistent result. That being said, you can still make your own async iterators that do whatever you want, including eagerly resolving the backlog of promises with done: true.

@devsnek Yes of course I could make my own version but it does not seem a good idea.

Anyway, I didn't know that async generators are buffering...are you referring to all async generators? Hoping I'm not asking too much, could you explain a little better the first half of your latter response? Thanks 😄

if we have this function:

async function* f() {
  for (let i = 0; i < 3; i += 1) {
    await timeout(3000);
    yield i;
  }
}

and i do this:

const it = f();
it.next();
it.next();
it.next();
it.next();

i have called next many times before even the first timeout in f completes, f has no idea if any of those next calls are done. each of those next calls returns a promise, and async functions have an internal queue of those promises which they will pop and resolve/reject when they hit yield points or throw an error or return or whatever.

@devsnek thanks for the explanation, it totally makes sense to me. We are not forced to await __next__ calls...

So when a yield point is encountered, the corresponding Promise in the queue will be dequeued and resolved. This process seems not conceptually dependent on the _yielded_ value. Therefore, referring to your first comment, I am not able to see why _yielding_ a Promise could be a problem.
The next __next__ call with its argument will be dequeued and the async gen will be able to go on.

"Nested" promises apart, I think I am not able to see the elephant in the room. Sorry 😅

with your own async iterator you can put a promise in the value field, I'm just not sure why you would want to.

with your own async iterator you can put a promise in the value field, I'm just not sure why you would want to.

@devsnek I don't care about the use cases, I'd like only to fully grasp the reasoning that led the TC39 to expressly discourage this way of using async iteration. Your comments are more than valid, but I am not able to see the full picture.

In the example above, next is called four times, and at none of those calls does the iterator know whether or not it is done, so it has to wrap the done with the value, instead of just the value being the promise.

In the example above, next is called four times, and at none of those calls does the iterator know whether or not it is done, so it has to wrap the done with the value, instead of just the value being the promise.

Yes this is the purpose of the async iteration: deferring the done. But after the async gen has solved each done, the corresponding value should be able to assume any type of value without relevant problems for the async iteration itself.

which it can. you just can't do that from an async generator because it unwraps promises at the boundaries, like an async function.

This seems answered; happy to reopen if not.

Was this page helpful?
0 / 5 - 0 ratings