-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
yield *
in an async iterator should not call .throw
when the inner iterator produces a rejected promise
#2813
Comments
I’d figured the reasoning here was that even though next() produced a normal completion for a valid iterator result, the promise represents the eventual “actual” completion. It seems consistent with how Promises are “flattened” elsewhere. I was confused by some other things here though which seemed to be saying that "throw" is only called in this one scenario, which isn’t true. I might be misunderstanding, but I mean these bits sounded like they concerned not only that value-rejects case (which is unique) but also the “directly produced a throw completion” cases (which aren’t):
The If this weren’t the case, the resumption model would end up “backwards” for throw completions uniquely: catch blocks wouldn’t be entered when |
Even if that were true, the consistent behavior would be to not call any methods, on the assumption that the generator will have already done its own cleanup in that case, in the same way that the spec does not call (But I don't think the view that the promise here represents the eventual “actual” completion is consistent with the rest of the spec; contrast the behavior of
In what other scenario does
That does not match my reading. Step 7.a.i of the For a sync generator, as far as I can tell, the only way that Try it yourself: let inner = {
[Symbol.iterator]: () => ({
next() {
throw 0;
},
throw(e) {
console.log('throw called with', e);
throw e;
},
return(e) {
console.log('return called with', e);
return {};
},
}),
};
function* outer() {
yield* inner;
}
for (let item of outer()) {
console.log(item);
} No engine I have on hand will invoke the "throw" method in this code.
It is those steps which make that happen, yes, but the point I am making is that if a generator has not been resumed with a throw completion then |
Thanks for clearing that up! I see it now. The/a reason I had the wrong idea about this might be related to the issue you’ve described (in a general way: “don’t fuss with the ‘inners’ when it’s not needed”). I’ve often hit (what I now see is) step 7.b.iii.6 when using generators for “manual” control flow because it’s easy to reach when I think you’re correct about the async gen case being incorrect and am left wondering if the error “replacement” at 7.b.iii.6 is unnecessary for similar reasons; if |
Yeah, I also find that behavior surprising. It seems more natural to propagate the original exception instead of clobbering it - especially given that the behavior for a missing That also matches how I think about these handlers - an implementation of |
On the other hand, if you tried to call |
Yeah, that occurred to me too as the likely rationale. In practice for me it’s meant adding generator wrappers around things that could have directly delegated to native iterables if not for that step, so I tend to think it’s creating rather than solving a problem (now that I understand what’s really happening there). I don’t know if that’s always the case. (But this is something apart from the issue you’ve described anyway — sorry to sidetrack & thanks for the new clarity.) |
Following up here: #2819 got consensus at the July 2022 plenary meeting. We're waiting on implementations before landing that PR, at which point this can be closed. |
Consider the following horrible program:
Per spec, this will print "throw called with 'rejection'". Specifically, the evaluation semantics for
yield*
step 7.a.vi will invokeAsyncGeneratorYield
passingPromise.reject('rejection')
, which will then beAwait
'd, which will throw, causing thereceived
alias to hold a throw completion. The following machinery then acts as if.throw
had been explicitly called onouter
, which is the only other way thatreceived
can hold a throw completion - the behavior in that case being to invoke.throw
oninner
.I think this possibility was simply overlooked during the spec'ing of async generators. This is the only place in the entire spec that
.throw
is called without a user explicitly invoking it at some point. (Previously I'd thought there were no such places.) And the aliases certainly imply that the algorithm is assuming that a throw completion must have come from the user, rather than from the iterator itself.This behavior seems wrong to me. Normally when consuming an iterator the spec will either a.) determine that the iterator itself is broken (e.g. it threw), in which case it is assumed to have done its own cleanup and the spec will not make further calls to any of its methods, or b.) determine that the iterator is fine but for some reason the consumer needs to exit early, in which case the spec will call
.return
(not.throw
) to give the iterator a chance to clean up.I am inclined to treat this as in (b). Nothing in the spec prevents custom async iterators from returning promises - see the snippet in this comment. So I think we ought to treat the
await iterResult.value
as being something the outer iterator is doing, rather than a violation of the iteration protocol, so that if thisawait
happens to throw then the inner iterator should be closed with.return
.I could probably be persuaded to treat this as in (a), i.e. to say that this is a violation of the iteration protocol and so the spec need not clean up the inner iterator.
The current behavior seems bad, though.
For comparison,
will call
.return
oninner
, in the above example. I would likeyield*
to match this case.For the snippet above, JavaScriptCore and V8 implement the spec (they print "throw called with rejection" and then "threw rejection"). SpiderMonkey, Graal, and XS only print "threw rejection" (they also don't close the inner iterator).
The text was updated successfully, but these errors were encountered: