Normative: Close sync iterator when async wrapper yields rejection by mhofman · Pull Request #2600 · tc39/ecma262 (original) (raw)

This updates Async-from-Sync Iterator Objects so that the async wrapper closes its sync iterator when the sync iterator yields a rejected promise as value. This also updates the async wrapper to close the sync iterator when throw is called on the wrapper but is missing on the sync iterator, and updates the rejection value in that case to a TypeError to reflect the contract violation. This aligns the async wrapper behavior to the yield* semantics.

Slides presented at TC39 plenary in January 2020.

Close on rejection

A rejected promise as value is transformed by the async wrapper into a rejection, which is considered by consumers of async iterators as a fatal failure of the iterator, and the consumer will not close the iterator in those cases. However, yielding a rejected promise as value is entirely valid for a sync iterator. The wrapper should adapt both expectations and explicitly close the sync iterator it holds when this situation arise.

Currently a sync iterator consumed by a for..await..of loop would not trigger the iterator's close when a rejected promise is yielded, but the equivalent for..of loop awaiting the result would. After this change, the iterator would be closed in both cases. Closes #1849

This change plumbs the sync iterator into AsyncFromSyncIteratorContinuation with instructions to close it on rejection, but not for return calls (as the iterator was already instructed to close), or if the iterator closed on its own (done === true).

Close on missing throw

If throw is missing on the sync iterator, the async wrapper currently simply rejects with the value given to throw. This deviates from the yield * behavior in 2 ways: the wrapped iterator is not closed, and the rejection value not a TypeError to indicate the contract was broken. This updates fixes both differences by closing the iterator, and throwing a new TypeError instance instead of the value provided to throw.

Since the spec never calls throw on an iterator on its own (it only ever forwards it), and that the async wrapper is never exposed to the program, the only way to observe this async wrapper behavior is through a program calling yield * with a sync iterator from an async generator, and explicitly call throw on that async iterator.