<!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>