<div dir="ltr">Overall, I really like this proposal. I don't think `try` should have a monopoly on controlling exception handling; unifying exceptional and non-exceptional control flow in a single syntactic construct has too many benefits for ergonomics and clarity, especially for checked exceptions. (It seems to me that something like the current proposal is what checked exceptions were originally "getting at.")<div><br></div><div>One thing I'm interested in is the interaction (or lack thereof) between switch expressions and automatic resource management (better known as try-with-resources). Currently, `try` is the only construct that can control exception handling, but it's also the only construct that can auto-close resources, and sometimes these two functions seem frustratingly poorly integrated, as in this example:</div><div><br></div><div>        try {<br>            try (BufferedReader r = new BufferedReader(new InputStreamReader(Files.newInputStream(Path.of("lines"))))) {<br>                return r.readLine();<br>            }<br>        } catch (IOException ex) {<br>            return "";<br>        }<br></div><div><br></div><div>The current proposal gives us an alternative to these nested `try` blocks:</div><div><br></div><div><div>        switch (new BufferedReader(new InputStreamReader(Files.newInputStream(Path.of("lines"))))) {<br>            case BufferedReader r -> try (r) {<br>                r.readLine()<br>            }<br>            case throws IOException _ -> ""<br>        }<br></div><div><br></div></div><div>This is a bit odd, though, because we need a `switch` purely for exception handling, and we need a `try` that's doing *no* exception handling but is simply there for resource cleanup. Additionally, there's a superficial similarity between the try-with-resources syntax and the switch expression syntax, which may lead to confusion if the latter starts doing the job of the former. This code, for example, looks correct and familiar but actually has a resource leak:</div><div><br></div><div>        switch (new BufferedReader(new InputStreamReader(Files.newInputStream(Path.of("lines"))))) {<br>            case BufferedReader r -> r.readLine()<br>            case throws IOException _ -> ""<br>        }<br></div><div><br></div><div>I point this out not to encourage syntax bikeshedding, but just to ask what role automatic resource management should play in this discussion. If `try` shouldn't have a monopoly on exception handling, maybe it shouldn't have a monopoly on automatic resource management either.</div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Tue, Dec 12, 2023 at 1:23 PM Brian Goetz <<a href="mailto:brian.goetz@oracle.com">brian.goetz@oracle.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><u></u>

  
  <div>
    <font size="4" face="monospace">Based on some inspiration from
      OCaml, and given that the significant upgrades to switch so far
      position it to do a lot more than it could before, we've been
      exploring a further refinement of switch to incorporate failure
      handling as well.  <br>
      <br>
      (I realize that this may elicit strong reactions from some, but
      please give it some careful thought before giving voice to those
      reactions.)<br>
      <br>
      <br>
      <br>
    </font># Uniform handling of failure in switch<br>
    <br>
    ## Summary<br>
    <br>
    Enhance the `switch` construct to support `case` labels that match
    exceptions<br>
    thrown during evaluation of the selector expression, providing
    uniform handling<br>
    of normal and exceptional results.<br>
    <br>
    ## Background<br>
    <br>
    The purpose of the `switch` construct is to choose a single course
    of action<br>
    based on evaluating a single expression (the "selector").  The
    `switch`<br>
    construct is not strictly needed in the language; everything that
    `switch` does<br>
    can be done by `if-else`.  But the language includes `switch`
    because it<br>
    embodies useful constraints which both streamline the code and
    enable more<br>
    comprehensive error checking.<br>
    <br>
    The original version of `switch` was very limited: the selector
    expression was<br>
    limited to a small number of primitive types, the `case` labels were
    limited to<br>
    numeric literals, and the body of a switch was limited to operating
    by<br>
    side-effects (statements only, no expressions.)  Because of these
    limitations,<br>
    the use of `switch` was usually limited to low-level code such as
    parsers and<br>
    state machines.  In Java 5 and 7, `switch` received minor upgrades
    to support<br>
    primitive wrapper types, enums, and strings as selectors, but its
    role as "pick<br>
    from one of these constants" did not change significantly.  <br>
    <br>
    Recently, `switch` has gotten more significant upgrades, to the
    point where it<br>
    can take on a much bigger role in day-to-day program logic.  Switch
    can now be<br>
    used as an expression in addition to a statement, enabling greater
    composition<br>
    and more streamlined code.  The selector expression can now be any
    type.  The<br>
    `case` labels in a switch block can be rich patterns, not just
    constants, and<br>
    have arbitrary predicates as guards.  We get much richer type
    checking for<br>
    exhaustiveness when switching over selectors involving sealed
    types.  Taken<br>
    together, this means much more program logic can be expressed
    concisely and<br>
    reliably using `switch` than previously.<br>
    <br>
    ### Bringing nulls into `switch` <br>
    <br>
    Historically, the `switch` construct was null-hostile; if the
    selector evaluated<br>
    to `null`, the `switch` immediately completed abruptly with<br>
    `NullPointerException`.  This made a certain amount of sense when
    the only<br>
    reference types that could be used in switch were primitive wrappers
    and enums,<br>
    for which nulls were almost always indicative of an error, but as
    `switch`<br>
    became more powerful, this was increasingly a mismatch for what we
    wanted to do<br>
    with `switch`.  Developers were forced to work around this, but the
    workarounds<br>
    had undesirable consequences (such as forcing the use of statement
    switches<br>
    instead of expression switches.)  Previously, to handle null, one
    would have to<br>
    separately evaluate the selector and compare it to `null` using
    `if`:<br>
    <br>
    ```<br>
    SomeType selector = computeSelector();<br>
    SomeOtherType result;<br>
    if (selector == null) { <br>
        result = handleNull();<br>
    }<br>
    else { <br>
        switch (selector) { <br>
            case X: <br>
                result = handleX();<br>
                break;<br>
            case Y: <br>
                result = handleY();<br>
                break;<br>
        }<br>
    }<br>
    ```<br>
    <br>
    Not only is this more cumbersome and less concise, but it goes
    against the main<br>
    job of `switch`, which is streamline "pick one path based on a
    selector<br>
    expression" decisions.  Outcomes are not handled uniformly, they are
    not handled<br>
    in one place, and the inability to express all of this as an
    expression limits<br>
    composition with other language features.<br>
    <br>
    In Java 21, it became possible to treat `null` as just another
    possible value of<br>
    the selector in a `case` clause (and even combine `null` handling
    with<br>
    `default`), so that the above mess could reduce to<br>
    <br>
    ```<br>
    SomeOtherType result = switch (computeSelector()) {<br>
        case null -> handleNull();<br>
        case X -> handleX();<br>
        case Y -> handleY();<br>
    }<br>
    ```<br>
    <br>
    This is simpler to read, less error-prone, and interacts better with
    the rest of<br>
    the language.  Treating nulls uniformly as just another value, as
    opposed to<br>
    treating it as an out-of-band condition, made `switch` more useful
    and made Java<br>
    code simpler and better.  (For compatibility, a `switch` that has no
    `case null`<br>
    still throws `NullPointerException` when confronted with a null
    selector; we opt<br>
    into the new behavior with `case null`.)<br>
    <br>
    ### Other switch tricks<br>
    <br>
    The accumulation of new abilities for `switch` means that it can be
    used in more<br>
    situations than we might initially realize.  One such use is
    replacing the<br>
    ternary conditional expression with boolean switch expressions; now
    that<br>
    `switch` can support boolean selectors, we can replace<br>
    <br>
        expr ? A : B<br>
    <br>
    with the switch expression<br>
    <br>
    ```<br>
    switch (expr) { <br>
        case true -> A;<br>
        case false -> B;<br>
    }<br>
    ```<br>
    <br>
    This might not immediately seem preferable, since the ternary
    expression is more<br>
    concise, but the `switch` is surely more clear.  And, if we nest
    ternaries in<br>
    the arms of other ternaries (possibly deeply), this can quickly
    become<br>
    unreadable, whereas the corresponding nested switch remains readable
    even if<br>
    nested to several levels.  We don't expect people to go out and
    change all their<br>
    ternaries to switches overnight, but we do expect that people will
    increasingly<br>
    find uses where a boolean switch is preferable to a ternary.  (If
    the language<br>
    had boolean switch expressions from day 1, we might well not have
    had ternary<br>
    expressions at all.)<br>
    <br>
    Another less-obvious example is using guards to do the selection,
    within the<br>
    bounds of the "pick one path" that `switch` is designed for.  For
    example, we<br>
    can write the classic "FizzBuzz" exercise as:<br>
    <br>
    ```<br>
    String result = switch (getNumber()) { <br>
        case int i when i % 15 == 0 -> "FizzBuzz";<br>
        case int i when i % 5 == 0 -> "Fizz";<br>
        case int i when i % 3 == 0 -> "Buzz";<br>
        case int i -> Integer.toString(i);<br>
    }<br>
    ```<br>
    <br>
    A more controversial use of the new-and-improved switch is as a
    replacement for<br>
    block expressions. Sometimes we want to use an expression (such as
    when passing<br>
    a parameter to a method), but the value can only be constructed
    using<br>
    statements: <br>
    <br>
    ```<br>
    String[] choices = new String[2];<br>
    choices[0] = f(0);<br>
    choices[1] = f(1);<br>
    m(choices);<br>
    ```<br>
    <br>
    While it is somewhat "off label", we can replace this with a switch
    expression:<br>
    <br>
    ```<br>
    m(switch (0) { <br>
        default -> { <br>
            String[] choices = new String[2];<br>
            choices[0] = f(0);<br>
            choices[1] = f(1);<br>
            yield choices;<br>
        }<br>
    })<br>
    ```<br>
    <br>
    While these were not the primary use cases we had in mind when
    upgrading<br>
    `switch`, it illustrates how the combination of improvements to
    `switch` have<br>
    made it a sort of "swiss army knife".<br>
    <br>
    ## Handling failure uniformly<br>
    <br>
    Previously, null selector values were treated as out-of-band events,
    requiring<br>
    that users handle null selectors in a non-uniform way.  The
    improvements to<br>
    `switch` in Java 21 enable null to be handled uniformly as a
    selector value, as<br>
    just another value.<br>
    <br>
    A similar source of out-of-band events in `switch` is exceptions; if
    evaluating<br>
    the selector throws an exception, the switch immediately completes
    with that<br>
    exception.  This is an entirely justifiable design choice, but it
    forces users<br>
    to handle exceptions using a separate mechanism, often a cumbersome
    one, just as<br>
    we did with null selectors:<br>
    <br>
    ```<br>
    Number parseNumber(String s) throws NumberFormatException() { ... }<br>
    <br>
    try { <br>
        switch (parseNumber(input)) { <br>
            case Integer i -> handleInt(i);<br>
            case Float f -> handleFloat(f);<br>
            ...<br>
        }<br>
    }<br>
    catch (NumberFormatException e) {<br>
        ... handle exception ...<br>
    }<br>
    ```<br>
    <br>
    This is already unfortunate, as switch is designed to handle "choose
    one path<br>
    based on evaluating the selector", and "parse error" is one of the
    possible<br>
    consequences of evaluating the selector.  It would be nice to be
    able to handle<br>
    error cases uniformly with success cases, as we did with null. 
    Worse, this code<br>
    doesn't even mean what we want: the `catch` block catches not only
    exceptions<br>
    thrown by evaluating the selector, but also by the body of the
    switch.  To say<br>
    what we mean, we need the even more unfortunate<br>
    <br>
    ```<br>
    var answer = null;<br>
    try { <br>
        answer = parseNumber(input);<br>
    }<br>
    catch (NumberFormatException e) {<br>
        ... handle exception ...<br>
    }<br>
    <br>
    if (answer != null) { <br>
        switch (answer) { <br>
            case Integer i -> handleInt(i);<br>
            case Float f -> handleFloat(f);<br>
            ...<br>
        }<br>
    }<br>
    ```<br>
    <br>
    Just as it was an improvement to handle `null` uniformly as just
    another<br>
    potential value of the selector expression, we can get a similar
    improvement by<br>
    handling normal and exceptional completion uniformly as well. 
    Normal and<br>
    exceptional completion are mutually exclusive, and the handling of
    exceptions in<br>
    `try-catch` already has a great deal in common with handling normal
    values in<br>
    `switch` statements (a catch clause is effectively matching to a
    type pattern.)<br>
    For activities with anticipated failure modes, handling successful
    completion<br>
    via one mechanism and failed completion through another makes code
    harder to<br>
    read and maintain.  <br>
    <br>
    ## Proposal<br>
    <br>
    We can extend `switch` to handle exceptions more uniformly in a
    similar was as<br>
    we extended it to handle nulls by introducing `throws` cases, which
    match when<br>
    evaluating the selector expression completes abruptly with a
    compatible<br>
    exception: <br>
    <br>
    ```<br>
    String allTheLines = switch (Files.readAllLines(path)) {<br>
        case List<String> lines ->
    lines.stream().collect(Collectors.joining("\n"));<br>
        case throws IOException e -> "";<br>
    }<br>
    ```<br>
    <br>
    This captures the programmer's intent much more clearly, because the
    expected<br>
    success case and the expected failure case are handled uniformly and
    in the same<br>
    place, and their results can flow into the result of the switch
    expression.<br>
    <br>
    The grammar of `case` labels is extended to include a new form,
    `case throws`,<br>
    which is followed by a type pattern:<br>
    <br>
        case throws IOException e: <br>
    <br>
    Exception cases can be used in all forms of `switch`: expression and
    statement<br>
    switches, switches that use traditional (colon) or
    single-consequence (arrow)<br>
    case labels.  Exception cases can have guards like any other pattern
    case.  <br>
    <br>
    Exception cases have the obvious dominance order with other
    exception cases (the<br>
    same one used to validate order of `catch` clauses in `try-catch`),
    and do not<br>
    participate in dominance ordering with non-exceptional cases.  It is
    a<br>
    compile-time error if an exception case specifies an exception type
    that cannot<br>
    be thrown by the selector expression, or a type that does not extend<br>
    `Throwable`.  For clarity, exception cases should probably come
    after all other<br>
    non-exceptional cases.  <br>
    <br>
    When evaluating a `switch` statement or expression, the selector
    expression is<br>
    evaluated.  If evaluation of the selector expression throws an
    exception, and<br>
    one of the exception cases in the `switch` matches the exception,
    then control<br>
    is transferred to the first exception case matching the exception. 
    If no<br>
    exception case matches the exception, then the switch completes
    abruptly with<br>
    that same exception.  <br>
    <br>
    This slightly adjusts the set of exceptions thrown by a `switch`; if
    an<br>
    exception is thrown by the selector expression but not the body of
    the switch,<br>
    and it is matched by an unguarded exception case, then the switch is
    not<br>
    considered to throw that exception.<br>
    <br>
    ### Examples<br>
    <br>
    In some cases, we will want to totalize a partial computation by
    supplying a<br>
    fallback value when there is an exception: <br>
    <br>
    ```<br>
    Function<String, Optional<Integer>> safeParse = <br>
        s -> switch(Integer.parseInt(s)) { <br>
                case int i -> Optional.of(i);<br>
                case throws NumberFormatException _ ->
    Optional.empty();<br>
        };<br>
    ```<br>
    <br>
    In other cases, we may want to ignore exceptional values entirely: <br>
    <br>
    ```<br>
    stream.mapMulti((f, c) -> switch (readFileToString(url)) {<br>
                        case String s -> c.accept(s);<br>
                        case throws MalformedURLException _ -> { };<br>
                    });<br>
    ```<br>
    <br>
    In others, we may want to process the result of a method like
    `Future::get`<br>
    more uniformly:<br>
    <br>
    ```<br>
    Future<String> f = ...<br>
    switch (f.get()) {<br>
        case String s -> process(s);<br>
        case throws ExecutionException(var underlying) -> throw
    underlying;<br>
        case throws TimeoutException e -> cancel();<br>
    }<br>
    ```<br>
    <br>
    ### Discussion<br>
    <br>
    We expect the reaction to this to be initially uncomfortable,
    because<br>
    historically the `try` statement was the only way to control the
    handling of<br>
    exceptions.  There is clearly still a role for `try` in its full
    generality, but<br>
    just as `switch` profitably handles a constrained subset of the
    situations that<br>
    could be handled with the more general `if-else` construct, there is
    similarly<br>
    profit in allowing it to handle a constrained subset of the cases
    handled by the<br>
    more general `try-catch` construct.  Specifically, the situation
    that `switch`<br>
    is made for: evaluate an expression, and then choose one path based
    on the<br>
    outcome of evaluating that expression, applies equally well to
    discriminating<br>
    unsuccessful evaluations.  Clients will often want to handle
    exceptional as well<br>
    as successful completion, and doing so uniformly within a single
    construct is<br>
    likely to be clearer and less error-prone than spreading it over two
    constructs.  <br>
    <br>
    Java APIs are full of methods that can either produce a result or
    throw an<br>
    exception, such as `Future::get`.  Writing APIs in this way is
    natural for the<br>
    API author, because they get to handle computation in a natural way;
    if they<br>
    get to the point where they do not want to proceed, they can `throw`
    an<br>
    exception, just as when they get to the point where the computation
    is done,<br>
    they can `return` a value. Unfortunately, this convenience and
    uniformity for<br>
    API authors puts an extra burden on API consumers; handling failures
    is more<br>
    cumbersome than handling the successful case.  Allowing clients to
    `switch` over<br>
    all the ways a computation could complete heals this rift.<br>
    <br>
    None of this is to say that `try-catch` is obsolete, any more than
    `switch`<br>
    makes `if-else` obsolete.  When we have a large block of code that
    may fail at<br>
    multiple points, handling all the exceptions from the block together
    is often<br>
    more convenient than handling each exception at its generation
    point.  But when<br>
    we scale `try-catch` down to a single expression, it can get
    awkward.  The<br>
    effect is felt most severely with expression lambdas, which undergo
    a<br>
    significant syntactic expansion if they want to handle their own
    exceptions.  <br>
    <br>
    <br>
  </div>

</blockquote></div>