[whatwg/streams] async iteration over a ReadableStream containing a rejected Promise will not clean up (Issue #1266)

https://github.com/whatwg/streams/pull/980 defined `Symbol.asyncIterator` semantics for streams, including notably:

- breaking out of a `for await` over a readable stream will cancel/release it, and
- `for await`ing over a readable stream which vends Promises will see the unwrapped values, consistent with async generators which `yield` Promises.

So, for example, the following code prints `{ item: 0 }` and then `"cleaning up"`:

```js
(async () => {
  let stream = new ReadableStream(
    {
      start(controller) {
        controller.enqueue(Promise.reject()); // NOTE: promise is now rejected
      },
      cancel() {
        console.log('cleaning up');
      },
    },
  );
  for await (let item of stream) {
    console.log({ item });
    break;
  }
})();
```
(You can run it in Firefox, which is the only implementation I'm aware of which has shipped async iteration for streams.)

But unfortunately there's an interaction between the two points above which leads to the stream dangling in the specific case that the stream vends a rejected promise:

```js
(async () => {
  let stream = new ReadableStream(
    {
      start(controller) {
        controller.enqueue(Promise.reject(0)); // NOTE: enqueuing a promise
      },
      cancel() {
        console.log('cleaning up');
      },
    },
  );
  try {
    for await (let item of stream) {
      console.log({ item });
      break;
    }
  } catch (e) {
    console.log('caught', e);
  }
})();
```
This will print `caught: 0`, and then nothing else. Specifically, it will _not_ print "cleaning up" - the stream does not get cleaned up at all.

This is because the `for await` machinery assumes that rejected promises signal the end of iteration, as they would in an async generator, and therefore assumes that the iterable has done its own cleanup and so the `for await` does not need to call `.return` to explicitly signal cleanup (as it normally does when stopping iteration early).

---

Possibly this is sufficiently obscure that it should just be ignored. But I do think it's a bug. I believe the fix would be to do your own cleanup in this case as if the `for await` loop had called `return` - that is, add a `catch` handler to the promise after the "Resolve promise with chunk" step in the [get the next iteration result steps](https://streams.spec.whatwg.org/#rs-asynciterator-prototype-next) algorithm, and have the `catch` handler invoke [the asynchronous iterator return steps](https://streams.spec.whatwg.org/#rs-asynciterator-prototype-return).

-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/streams/issues/1266
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/streams/issues/1266@github.com>

Received on Wednesday, 5 April 2023 05:20:19 UTC