As we move towards the next deliverable -- record patterns -- we have two new questions regarding exceptions to answer.

#### Questions

1.  When a dtor throws an exception.  (You might think we can kick this down the road, since records automatically acquire a synthetic dtor, and users can't write dtors yet, but since that synthetic dtor will invoke record component accessors, and users can override record component accessors and therefore they can throw, we actually need to deal with this now.)

This has two sub-questions:

1a.  Do we want to do any extra type checking on the bodies of dtors / record accessors to discourage explicitly throwing exceptions?  Obviously we cannot prevent exceptions like NPEs arising out of dereference, but we could warn on an explicit throw statement in a record accessor / dtor declaration, to remind users that throwing from dtors is not the droid they are looking for.

1b.  When the dtor for Foo in the switch statement below throws E:

    switch (x) {
        case Box(Foo(var a)): ...
        case Box(Bar(var b)): ...
    }

what should happen?  Candidates include:

 - allow the switch to complete abruptly by throwing E?
 - same, but wrap E in some sort of ExceptionInMatcherException?
 - ignore the exception and treat the match as having failed, and move on to the next case?

2.  Exceptions for remainder.  We've established that there is a difference between an _exhaustive_ set of patterns (one good enough to satisfy the compiler that the switch is complete enough) and a _total_ set of patterns (one that actually covers all input values.)  The difference is called the _remainder_.  For constructs that require totality, such as pattern switches and let/bind, we have invariants about what will have happened if the construct completes normally; for switches, this means exactly one of the cases was selected.  The compiler must make up the difference by inserting a throwing catch-all, as we already do with expression switches over enums, and all switches over sealed types, that lack total/default clauses.

So far, remainder is restricted to two kinds of values: null (about which switch already has a strong opinion) and "novel" enum constants / novel subtypes of sealed types. For the former, we throw NPE; for the latter, we throw ICCE.

As we look ahead to record patterns, there is a new kind of remainder: the "spine" of nested record patterns.  This includes things like Box(null), Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc.  It should be clear that there is no clean extrapolation from what we currently do, to what we should do in these cases.  But that's OK; both of the existing remainder-rejection cases derive from "what does the context think" -- switch hates null (so, NPE), and enum switches are a thing (which makes ICCE on an enum switch reasonable.)  But in the general case, we'll want some sort of MatchRemainderException.

Note that throwing an exception from remainder is delayed until the last possible moment.  We could have:

    case Box(Bag(var x)): ...
    case Box(var x) when x == null: ...

and the reasonable treatment is to treat Box(Bag(var x)) as not matching Box(null), even though it is exhuastive on Box<Bag<?>>), and therefore fall into the second case on Box(null).  Only when we reach the end of the switch, and we haven't matched any cases, do we throw MatchRemainderException.

#### Discussion

For (1a), my inclination is to do nothing for record accessors, but when we get to explicit dtors, warning on explicit throw is not a bad idea.  Unlike ctors, where exceptions are part of the standard toolbox, deconstructors are handed an already-constructed object, and are supposed to be total.  If you're inclined to write a partial dtor, you're probably doing it wrong.  As it is a new construct, the additional error checking to guide people to its proper use is probably reasonable (and cheap.)

For (1b), since an exception in a dtor is suppose to indicate an exceptional failure, I don't think swallowing it and trying to go on with the show is a good move.  My preference would be to wrap the exception, as we do with ExceptionInInitializerError, to make it clear that an exception from a dtor is a truly unexpected thing, and clearly name-and-shame the offending dtor.  So there's a bikeshed to paint for what we call this exception.

For (2), trying to repurpose either NPE or ICCE here is a losing move.  Better to invent an exception type that means "uncovered remainder" (which is more akin to an IAE than anything else; someone passed a bad value to an exhaustive-enough switch.)  We would use the same exception in let/bind, so this shouldn't have "switch" in its name, but probably something more like MatchRemainderException.

Reply via email to