tc39 / proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Other
375 stars 29 forks source link

How might "else" integrate with targeted exception handling? #35

Closed zenparsing closed 8 years ago

zenparsing commented 8 years ago

Using spider-monkey-style catch guards as a strawman:

try {
  f();
} catch (err if err instanceof Cancel) {
  // a
} catch (err) {
  // b
} else (err if err instanceof Foo) {
  // c
} else (err) {
  // d
}

Would any of those be disallowed in a future with some kind of catch guard syntax?

domenic commented 8 years ago

We cannot allow both else and catch in the same try block, as otherwise there are ambiguity issues, I believe:

if (a)
  try { } catch (e) { }
  else (e) { } // belongs to try or belongs to if?

If I understand correctly you could not allow both guarded and unguarded versions for similar reasons:

if (a)
  try { } else (e if e instanceof Foo) { }
  else (e) { } // belongs to try or belongs to if?

You have a similar problem with alternatives to else (see #31):

try { }
except (e if e instanceof Foo) { }
except (e) { } // function call except(e) followed by empty block)
zenparsing commented 8 years ago

I see. So if we choose any identifier or any keyword which can appear immediately after a block, we won't be able to have a catch-guard syntax that uses multiple clauses. Unless there's some distinguishing token following the "else" where we can use lookahead.

domenic commented 8 years ago

@zenparsing this issue is vexing me :(. Did you have any ideas? /cc @allenwb since he was helpful in pointing out that we don't necessarily need to use a keyword in the first place.

So if we choose any identifier or any keyword which can appear immediately after a block, we won't be able to have a catch-guard syntax that uses multiple clauses.

I think to be more precise, we won't be able to have a catch-guard syntax that also allows an unguarded clause.

Is it possible to say that except (e) { } following a guarded except clause (as in my "You have a similar problem" example) is always an unguarded except clause, and not a function call followed by an empty block? I wouldn't have thought so, but on the other hand, it seems pretty similar to saying that in try {} except (e) {} we interpret except (e) {} as an except clause, instead of a syntax-error-causing function call followed by empty block.

Unless there's some distinguishing token following the "else" where we can use lookahead.

Do you have any ideas on making this palatable? Everything I can come up with hurts the narrative of "else/except is the new catch" by making it uglier and more verbose. E.g. catch* (e) { } or else catch (e) { } or similar.

ljharb commented 8 years ago

is catch error (e) {} not an option?

domenic commented 8 years ago

It seems like a possible fallback, but it really fails the "catch error is the new catch" story by being too verbose.

littledan commented 8 years ago

Why can't we allow them to be interspersed freely? I'd imagine that if you have several catch guards, they are evaluated one at a time sequentially, and the first one that "hits" gets used. except could fit cleanly into this scheme: you'd probably put it towards the end of your list, but if you put it before some guarded catches, those ones probably won't hit.

domenic commented 8 years ago

@littledan the problem is the ambiguities in https://github.com/domenic/cancelable-promise/issues/35#issuecomment-238437960.

Or do you think there's a way to make those unambiguous? Which interpretation would we even want? I feel like in the third case it's pretty clear you want except(e) { } to be attached to the try, so if we go for except instead of else then maybe we have an escape hatch. But the first two (with else) are not clear to me. And I'm not familiar enough with the grammar machinery to know if it's possible to say that except(e) { } should be interpreted as attached to the try sometimes, but interpreted as a function call with an empty block other times.

littledan commented 8 years ago

Sorry for skimming too quickly over the intermediate thread. I see a couple solutions here:

domenic commented 8 years ago

Thanks! Let me see if I understand those first two...

Leave the guarded ones as catch, and use except for the cancel guard.

Meaning, you wouldn't be able to use guards with except syntax; you'd fall back to catch if you wanted guards? Messes with the except is the new catch story...

As for the else ambiguity for multiple else guarded versions, I'm not sure that's so bad. We already have it for ordinary if statements. Just add some more braces.

What does this mean in practice? Would you be saying that the else belongs to the if in these cases, and that's ok---if you don't like that, use more braces? My original worry was that the ambiguity would mean some kind of contradiction or incoherency in the grammar rules, but maybe that's silly and the grammar machinery can work with it just fine.

If I'm understanding this correctly, then I like this alternative the best.

Use another keyword besides else and except. I heard protected is free :)

Yeah, this is definitely a solution. Everything is pretty bad though. break, protected, case, and with are less worse than others :).

bergus commented 8 years ago

Maybe it's an alternative to drop the whole new clause thing and do it with the old catch and finally, so that we don't need any "except is the new catch" story at all. Let's make it "catch works differently in async functions"!

We could make await.cancelToken more powerful by letting catch clauses ignore any cancellation values from the token. At the function boundary, the cancellation would become the plain old rejection again. The rule basically would be to wrap all catch blocks inside an async function in an additional, implicit if (await.cancelToken === undefined || !await.cancelToken.requested) { … } block.

That way, catch does not get triggered by cancellation but finally does (as everybody expects), catch does not become broken (its new special behaviour would be local and limited by await.cancelToken), and it is trivial to combine a catch clause (for exceptions) with a finally { if (token.requested) clause (for cancellation handling).

littledan commented 8 years ago

catch is already present in async functions. I think it's an important design goal of async functions that they operate like sync ones in all other language semantics respects.

bergus commented 8 years ago

@littledan There are no language semantics for synchronous cancellation yet, and I believe that we don't need any as well. If at all, you can compare it with a return statement inside a try block. That catch is already present in async functions is not a problem - it's behaviour would not change until you use async.cancelToken. Another benefit of this solution would be that an ESnext-to-ES7 transpiler would not need to transpile except clauses everywhere, it would only need to transform async functions that use async.cancelToken.