<!DOCTYPE html><html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  </head>
  <body>
    <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>
  </body>
</html>