Closed domfarolino closed 4 weeks ago
The purpose of CreateAsyncFromSyncIterator
isn't to guarantee that [[NextMethod]]
returns a Promise; rather, it's to lift an IteratorResult<Promise<T>>
to Promise<IteratorResult<T>>
.
That is, if a sync iterator returns { next: Promise.resolve(0), done: false }
, then CreateAsyncFromSyncIterator
turns that into Promise.resolve({ next: 0, done: false })
. Note that the next
field no longer contains a Promise. That, and only that, is the purpose of CreateAsyncFromSyncIterator
. The fact that this also happens to guarantee the result is a Promise is just a side-effect.
Consumers generally shouldn't have to care whether the result of next
is a Promise; rather, they should just do whatever the local equivalent of await
is. await
can consume either a Promise or non-Promise value (or other kinds of thenable, for that matter) - as you note, mechanically this is specified in terms of constructing an actual Promise value and then waiting on that (unless the original thing was a real Promise according to certain rules), though there's no reason a engine would actually need to implement it this way. Using await
is the normal way ECMAScript consumes things which are expected to be Promises, and I would expect other consumers to follow suit unless there's specific reason to do something else, both here and also anywhere else they're consuming Promises.
OK great, this is a super useful response and great information. Thanks a lot! I'll close this.
GetIterator(obj, ASYNC)
returns an Iterator Record with a[[NextMethod]]
that is pulled from the user-supplied object. The next method hopefully returns a Promise, but this requirement is not enforced. Because it is not enforced, specs that convert values to async iterators this way must take care to wrap the resulting value in a Promise. This is standard practice; the Streams Standard does this, and so do the semantics of for await loops: specifically, the head evaluation semantics grab an async iterator record, and then step 6 of the body evaluation gets the next value and unconditionallyAwait()
s it.Await()
first wraps the value in a Promise and awaits it, which is necessary in case the value returned from the author's async iterable'snext()
method is not a Promise.In other words, the consumer of an async iterable is responsible for wrapping all
next()
values in a Promise, in case the code author did not do this. This is understandable, but a little annoying I guess, since responsibility is pushed to the consumer when ECMAScript itself could take care of this.In fact, ECMAScript does take care of this (i.e., ensuring that all
next()
values are Promises) but only whenGetIterator(obj, ASYNC)
falls back to the@@iterator
implementation (i.e., when@@asyncIterator
is not present). In this case and this case only, the fallback prose doesn't just directly use the sync iterator Record for itsnext()
values; instead, it delegates toCreateAsyncFromSyncIterator()
, which creates an internal%AsyncFromSyncIteratorPrototype%
object whosenext()
method is guaranteed to return a Promise that resolves to the underlying[[SyncIteratorRecord]]
's actual next value (which should be an Iterator Result).This automatic Promise-wrapping is quite nice, but it's strange that we only do it for the least kind of async iterable, i.e., sync iterables. Since we have this automatic Promise-wrapping semantics sometimes, it'd be nice if we could extend it to all converted async iterables, so that consumers of
GetIterator(obj, ASYNC)
could always guarantee that the result ofIteratorNext()
is a Promise.If we don't, then since consumers of async iterables are anyways trained to do the wrapping themselves, maybe we can just get rid of the
CreateAsyncFromSyncIterator()
usage in the sync fallback case. After all, there's no need for[[NextMethod]]
to always return a Promise, since consumers can't rely on this in general.Thoughts?