Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async iteration can produce promises, which the ES spec generally avoids #1288

Closed
bakkot opened this issue Apr 5, 2023 · 2 comments
Closed

Comments

@bakkot
Copy link
Contributor

bakkot commented Apr 5, 2023

Consider:

(async () => {
  let stream = new ReadableStream(
    {
      start(controller) {
        controller.enqueue(Promise.resolve(0)); // NOTE: enqueuing a promise
        controller.close();
      },
    },
  );
  for await (let item of stream) {
    console.log({ item });
  }
})();

(Note that ReadableStreams are async iterable as of whatwg/streams#980, but that's only implemented in Firefox as of this writing.)

Should this log 0, or a Promise for 0? My reading of the spec says a Promise for 0. Firefox, the only implementation, says an unwrapped 0.

The Asynchronous iterable declarations and Asynchronous iterator prototype object sections in webidl describe the relevant wiring for async iteration. From what I can tell, per step 8.5.4.4 of this algorithm, there is no unwrapping for promises. This is in contrast to ES async generators, which (as you can see in the definition of Yield) will unwrap Promises before yielding them.

That means iterating over a ReadableStream, or any other async iterable which can produce Promises, will let you observe a Promise in a for await loop. That's arguably a contract violation, per the original design of async iteration.

Possibly webidl should enforce that Promises are unwrapped here, like async generators do. (See brief discussion in #WHATWG.)

Though note that there might be some complexity about how to handle rejected Promises - for await treats promise rejection as the iterable closing itself, which means it doesn't call the return method, which would prevent running the asynchronous iterator return steps (if any) to do cleanup. So if you go this route, it's possible that unwrapping a rejected Promise will need to explicitly trigger those steps to ensure cleanup happens.

(edit: as pointed out below the spec does in fact handle the flattening already, but it also fails to handle rejected Promises in the way pointed out in the previous paragraph, for which I've opened whatwg/streams#1266.)

@TimothyGu
Copy link
Member

TimothyGu commented Apr 5, 2023

Hmm, I'm not sure if the problem actually exists. Your concern is that value at step 8.5.4.4 (aka next) is a promise. But next was received from nextPromise, which is the result of getting the next iteration result for a ReadableStream. That algorithm returns promise, which is resolved with chunk. chunk could potentially be a promise, but resolving promise with chunk essentially unwraps chunk.

As far as I know, it's impossible to create an ES promise whose fulfilled value is another promise. Since the interface between this spec and other specs ("get the next iteration result") is a promise, the problem of doubly-wrapped promises should (in theory) never happen.

@bakkot
Copy link
Contributor Author

bakkot commented Apr 5, 2023

Oh, that's quite right. I missed that the interface between the two specs was phrased in terms of a promise for a value directly.

@bakkot bakkot closed this as completed Apr 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants