Generator return() and exceptions
At one point I suggested privately that the return() method should throw a specified exception instead of causing a "return". The TC39 notes from the last meeting do not record any discussion of this.
I understand that one of the concerns about this approach was about guards and catch blocks, but making return() throw an exception does not affect that in the least: it's already the case that any catch block may see any old exception. Adding a new kind of exception doesn't affect that; properly written catch blocks have to test the positive presence of the exception they are looking for, not the absence of exceptions they aren't interested in. Magical return() is no better here because, as you note, return can be caught too.
There are valid cases in which you can have a catch without a guard; these are when can enumerate the possible exceptions that a part of your program will throw. However this condition necessarily limits the size of the try{} block -- it is unlikely to contain a yield. So no problem there either.
Magical returns seem unnecessary to me.
Andy
Apologies for the duplicate mail, how goofy of me. The second one was the edit I wanted but they are the same.
On Jul 23, 2014, at 1:25 AM, Andy Wingo wrote:
The TC39 notes do not record any discussion of return() causing an exception to be thrown.
In the latest ES6 draft for-of propagates any exceptions thrown by the call to return(). See people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-forin-div-ofbodyevaluation step 3.k.ii.1-2
As a matter of design policy we rarely, if ever, just drop exceptions.
I understand that one of the concerns was about guards and catch blocks, but making return() throw an exception does not affect that in the least.
It's already the case that any catch block may see any old exception. Adding a new kind of exception doesn't affect that; properly written catch blocks have to test the positive presence of the exception they are looking for, not the absence of exceptions they aren't interested in. Magical return() is no better here because, as you note, return can be caught too.
There are valid cases in which you can have a catch without a guard; these are when can enumerate the possible exceptions that a part of your program will throw. However this condition necessarily limits the size of the try{} block -- it is unlikely to contain a yield. So no problem there either.
It's not exactly clear to me what your point is here. If you want to catch any exception (including those originating from a return() call to the iterator) that can occur within while executing a for-of statement you would code:
try {
for (let each of expr) {...}
} catch (e) {... };
If you only want to catch exceptions from the for-of body you would code:
for (let each of expr) {
try { ...
} catch (e) { ...};
}
On Wed 23 Jul 2014 18:19, Allen Wirfs-Brock <allen at wirfs-brock.com> writes:
In the latest ES6 draft for-of propagates any exceptions thrown by the call to return(). See people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics- forin-div-ofbodyevaluation step 3.k.ii.1-2
As a matter of design policy we rarely, if ever, just drop exceptions.
I probably didn't explain myself completely; apologies. I meant that
the mechanism of iter.return()
should be implemented by throwing an
exception (i.e., as if by iter.throw(new StopIteration)
) instead of
"returning" from the yield point.
Well, in ES6 we don't define iterator termination in terms of a StopIteration
exception.
However, both gen.throw and gen.return are specified in terms of abnormal completion of a yield:
- people.mozilla.org/~jorendorff/es6-draft.html#sec-generator.prototype.return
- people.mozilla.org/~jorendorff/es6-draft.html#sec-generator.prototype.throw
so they both unwind in a similar manner.
consider:
function gen() {
try {
while (true) {
try {yield foo()
} catch (e) {
console.log("throw");
throw (e);
};
}
} finally {
console.log("unwind");
};
}
var g = gen();
g.next();
g.throw(new Error); //logs: throw, unwind
var h = gen();
h.next();
h.return(); //logs: unwind
Now that we have return() it isn't clear to me that we actually need throw() or whether for-of/yield* should call throw() like they currently do.
This is something I hope to discuss at the upcoming TC39 meeting.
Andy Wingo wrote:
I probably didn't explain myself completely; apologies. I meant that the mechanism of
iter.return()
should be implemented by throwing an exception (i.e., as if byiter.throw(new StopIteration)
) instead of "returning" from the yield point.
You may have missed it, but way back in ES4 days, Igor Bukanov and I implemented generators based in large part on Python 2.5 in SpiderMonkey. We didn't implement GeneratorExit (the exception thrown by close at the generator parked in a yield). Instead, Igor argued here and on python-dev in favor of forcing a return, and Phillip J. Eby among other Python leaders agreed that was better (and perhaps Python would move that way in the future).
mail.python.org/pipermail/python-dev/2006-August/068429.html
is the head of the python-dev thread.
esdiscuss/2006-December/000297
is the oldest es-discuss hit I can find quickly.
I think aesthetics are better, but de gustibus. Beyond the ugliness of implicit throw and a magic singleton per realm to throw, not polluting catch clauses in the field with a new exception object seems like a substantive win to me, as to Igor, Phillip , et al. People do write unguarded catches and make assumptions about what they caught. They shouldn't, but they do.
So I see some risk in adding a new exception and built-in throw. The risk of catching a return seems strictly much-less-than, at a glance.
Allen Wirfs-Brock wrote:
Now that we have return() it isn't clear to me that we actually need throw() or whether for-of/yield* should call throw() like they currently do.
Wait, throw is useful anyway, for exception-throwing apart from returning. Right?
Also I do not know what you mean by "like they currently do".
I hope this is all pre-caffeine confusion on my part!
Now that we have return() it isn't clear to me that we actually need throw()
Am I understanding correctly that you're suggesting removing throw
from
Generator.prototype
?
If so, I'd like to suggest that, at a minimum, .throw()
has been rather
useful in the context of Node.js control-flow libraries that surface
"catchable" asynchronous errors.
On Jul 23, 2014, at 11:12 AM, Brendan Eich wrote:
Wait, throw is useful anyway, for exception-throwing apart from returning. Right?
Also I do not know what you mean by "like they currently do".
I hope this is all pre-caffeine confusion on my part!
Here's what I'm trying to say:
for (let each of aGenerator) {
return 42;
}
I think we all agree, that the loop implicitly show do a:
aGenerator.return(42); //or perhaps just: aGenerator.return() ??
but what about:
for (let each of aGenerator) {
throw new Error;
}
Do we do:
aGenerator.throw(theErrorExceptionObject); //propagate all unhanded exceptions that terminated the loop to the generator
or
aGenerator.return(); //unwind the generator because the loop is abnormally terminating, then propagate the exception
The current spec. draft does the first of these, but I'm not totally convinced that is the better alternative.
To me, it seems to come down to how yo think about the generator in relationship to its clients.
If you think about it from the perspective of the generator, then one model is that a yield
is really just a callback into the client loop and the generator would expect its exception handlers to catch unhanded exceptions generated within the callback.
If you think about it from the perspective of the loop, the generator is just an (iterator) object on which you are performing 'next' method calls. There is no particular reason that you would expect an exception occurring in the loop body (or from the loop mechanism) to propagate to the iterator object (or any other known object) as it isn't in the current call chain. But if the loop abnormally terminates by an unhandled exception you still want to "close" the iterator by invoking its 'return' method.
yield* is similar to a loop, in that (as currently specified) it propagates a throw() delivered to the outer generator to the inner iterator. Does this really make sense? What relevance is that exception to the inner iterator? Why shouldn't yield * just call return() on the inner iterator to "close" it?
So going back to my original questions. Given that we now have return() available to close an abnormally discarded generator, when would somebody (presumably implementing a control abstraction) actually want to apply throw() rather than return() to an iterator?
catch and finally are really two quite different things. Catch is a dynamically scoped mechanism for finding exception handlers. Finally, is a unwind mechanism for cleaning up local invariants when a function activation is abnormally terminated. It's clear, to me, why a control abstraction would want to unwind a generator (ie return()) it is discarding. It's less clear (at least seemingly a lot less common) for a control abstraction to want to splice into the exception handling of a generator).
I guess what I need to see are some motivating user cases for generator throw() where throw is really what is needed rather than return();
Finally, when thinking about these issue if find that 'close' does seem to be a more appropriate name than 'return'. The mechanism is based upon using a 'return' completion value, but the callers intent seems to be 'close the iterator' (or unwind the iterator).
Also, I find some of these questions seem simpler to reason about I think in terms of an iterator with 'next', 'throw' and 'return' methods rather than a generator and its various internal states.
On Jul 23, 2014, at 11:21 AM, Jeremy Martin wrote:
Am I understanding correctly that you're suggesting removing throw from Generator.prototype?
If so, I'd like to suggest that, at a minimum, .throw() has been rather useful in the context of Node.js control-flow libraries [1] that surface "catchable" asynchronous errors.
I see, your example is using generators like the first perspective I described in my response to Brendan. The generator is essentially in control and sees the 'yield' as a call to an operation for which it is prepared to handle exceptions. It's logically splicing together two call chains. Essentially using a generator as a co-routine.
Ok, I buy that throw() is useful for that use case. I still think it is a different from the looping control structure case where the loop is in control rather than the iterator.
Allen Wirfs-Brock wrote:
Also, I find some of these questions seem simpler to reason about I think in terms of an iterator with 'next', 'throw' and 'return' methods rather than a generator and its various internal states.
The loop forms are "external iteration" (esdiscuss.org/topic/generators-vs-foreach) and any throw or return within the body should not propagate a useful value to the generator implementation of the iterator (if any). The contract is based only on .next().
Reifying control effects as exceptions or implicitly invoked methods needs a strong rationale. Implicit is worse than explicit. Now is not the time to invent without extant use cases.
I agree, for-of shouldn't do implicit throws() to its iterator, just return(). However yield* has been specified for a long time to implicitly propagate a throw() to the outer generator into as a throw() to the inner iterator. Maybe that made sense in the absence of return() but now that we have return (). yield* is really just a loop that the throw() is terminating early. From that perspective it seens we should invoke return() and not throw on the inner iterator.
Allen Wirfs-Brock wrote:
Maybe that made sense in the absence of return() but now that we have return (). yield* is really just a loop that the throw() is terminating early. From that perspective it seens we should invoke return() and not throw on the inner iterator.
Good point, sorry I missed that critical * in your earlier post.
Allen Wirfs-Brock wrote:
So going back to my original questions. Given that we now have return() available to close an abnormally discarded generator, when would somebody (presumably implementing a control abstraction) actually want to apply throw() rather than return() to an iterator?
Sorry, I may be misunderstanding, but what about the use from Task.js?
mozilla/task.js/blob/master/lib/task.js#L395
var foo = async(function*() {
throw 'from foo';
});
var bar = async(function*() {
var promise = foo(),
value;
try {
value = yield promise;
} catch (e) {
console.log(e);
}
});
The above should log "from foo". Wouldn't throw()
be needed for that?
On Jul 24, 2014, at 10:38 AM, Nathan Wall wrote:
The above should log "from foo". Wouldn't
throw()
be needed for that?
Right, that's the use case I was lookingor. Also mentioned by Jeremy in an earlier response.
The TC39 notes do not record any discussion of return() causing an exception to be thrown.
I understand that one of the concerns was about guards and catch blocks, but making return() throw an exception does not affect that in the least.
It's already the case that any catch block may see any old exception. Adding a new kind of exception doesn't affect that; properly written catch blocks have to test the positive presence of the exception they are looking for, not the absence of exceptions they aren't interested in. Magical return() is no better here because, as you note, return can be caught too.
There are valid cases in which you can have a catch without a guard; these are when can enumerate the possible exceptions that a part of your program will throw. However this condition necessarily limits the size of the try{} block -- it is unlikely to contain a yield. So no problem there either.
WDYT? I really really really do not want magical returns.
Andy