Exception handling in switch

ram anvesh reddy ramanvesh at gmail.com
Fri Jul 12 18:50:47 UTC 2024


Comments:
----------------
The motivation of this feature is well founded. We need a way to handle
exceptional scenarios and normal data flows in a uniform way. That said I
can feel some of the same scepticism expressed in the experts group
regarding adding `case throws` to the switch block.

One major problem I see is having to learn that the 'normal' cases and the
'exceptional' are mutually exclusive: Flow cannot shift from one to the
other. When I am coding normal cases I am thinking "This won't match this
case, so will check the next case and so on -  so the ordering is
semantically important. Whatever way we introduce this feature, it has to
'model' this mutual exclusivity in the structure of the code so that it is
plain and obvious.

One more related problem I see is that I have to learn that an exception in
any of the normal case arms will not be caught in the exceptional arms -
Here again the ordering of the exceptional cases becomes important - i
would then have to write all the exceptional arms first, and then the
normal arms, just so that someone reading my code doesn't get confused -
but this is not mandated by the language grammar. Then there is this
confusion of interleaved normal and exceptional cases which the JEP
recommends against like this: "It is strongly recommended to group normal
cases together and exception cases together". While this might be ok - and
IDEs/auto-formatters can auto-group these cases without loss of semantics
(albeit with ensuing format wars - should exceptional arms be first or
last?) - to me it looks like a hint that something is missing in the way we
are modelling the structure of the code.

There are other minor issues I have with the syntax proposed in the JEP -
I will address those in the next section;

In the same thread, Brian spoke about the try monad:

>  switch (try e) {
>         case Success(P1) -> …
>         case Success(P2) -> …
>         case Failure(E1) -> …
>     }
>
> This I think gets to the root of the issue:

> We explored this point as well in the exploration, and backed off.
>
> I would request to go down this road a bit further and not back off. It
seems to me that even if we do not actually implement the try monad,
completely hashing out a theoretical mental model around the try monad will
help us model the language in the right direction - at the very least
helping us avoid ending up creating different ways of doing the same thing.

========================================



Suggestion:
-----------------------
To me it looks like the right direction would be something like "enhanced
catch blocks" with the grammar of the catch block mimicking the switch
block. The idea being for this to become a uniform way to handle exceptions
in *any* place in java - not just in the enhanced switch.

Examples:
1. traditional Try block:
try {
  doSomething();
  thenSomething();
} catch {
  IllegalArgumentException e -> handle1(e);
  IllegalStateException _ -> handle2();
  NoSuchElementException | MissingConfigException lub -> handle3();
  default e -> handleDefault(e);
}

Advantages:
Works with existing try blocks syntax
The enhanced catch clause looks more like a switch - less ceremony, more
actual code

Notes:
It supports the same dominance hierarchy as the old catch, unnamed
variables, multi catch, nothing new to learn
default e compiles to Throwable e - so that devs don't forget to catch some
nonException Throwables  (This can be skipped if it causes more problems
than it solves)


2. With enhanced switch:

switch try (doSomething().thenSomething())
catch {
  IllegalArgumentException e -> handle1(e);
  IllegalStateException _ -> handle2();
  NoSuchElementException | MissingConfigException lub -> handle3();
  default e -> handleDefault(e);
} {
  case a -> normal1(a);
  case b-> normal2(b);
  default -> noop();
}

Advantages:
Mutual exclusivity!
Catch comes first always as it corresponds to the try- no format wars
No repetition of 'case throws' on each line - less ceremony - the catch
outside the block gives the necessary context. (Tangent: Can we similarly
move 'case's outside the case block and avoid the case repetitions?)
No wars about 'should it be case throws or case throw or case catch or just
catch' (I support just catch followed by case throws FWIW)
Clean mental model: switch has case arms, switch try has catch arms and
case arms (no need for normal/exceptional terminology)

Challenges:
A little more verbose with few extra braces.
Some might balk at the fact that the exception handling comes first - but I
think this is temporary. In the long term, the unambiguity of the fact that
any exception in the normal arms will not be handled by the catch arms is
worth much more IMO.

3: Enhanced switch try with an enclosing try intended to catch exceptions
thrown from the case arms and any from the catch arms

try{
  switch try (doSomething().thenSomething())
    catch {
      IllegalArgumentException e -> handle1(e);
      IllegalStateException _ -> handle2();
      NoSuchElementException | MissingConfigException lub -> handle3();
      default e -> handleDefault(e);
  } {
    case a -> normal1(a);
    case b-> normal2(b);
  }
} catch {
  UberException u -> x();
  OtherException o -> y();
  default e -> z();
}

Advantages:
Composes very cleanly with an uber try catch designed to catch exceptions
thrown from the case and catch arms.

4. Enhanced catch with traditional switches:

switch try (doSomething().thenSomething())
catch {
  IllegalArgumentException e -> handle1(e);
  IllegalStateException _ -> handle2();
  NoSuchElementException | MissingConfigException lub -> handle3();
  default e -> handleDefault(e);
} {
  case a:
    normal1(a);
    break;
  case b:
    normal2(b);
    break;
  default:
    noop();
}

Advantages:
Allows devs to start doing better exception handling without messing with
old switches!

5: <<FUTURE>> Try expressions:

var value = try (doSomething().thenSomething())
catch {
  IllegalArgumentException e -> v1;
  IllegalStateException _ ->  yield v2 ;
  NoSuchElementException | MissingConfigException lub ->  v3;
  default e -> throw e;
}

Advantages:
This is exactly similar to the switch try syntax just without the switch
and case arms.
The yielding works exactly like the case arms
This exact syntax will start working if and when the try monad comes - the
expression ' doSomething().thenSomething()' can be replaced by the monad
variable 'result' etc.
Similarly we can extend this to make the switch try itself a 'switch try
expression', again same syntax

Mental model:
------------
Switch switches across values (normal cases)
Catch switches across exceptions

Catch always catches what is in the try
Case is for what is in switch

'switch try' combines switch and try - giving us 'catch'-ability and
'case'-ability

Developers have these options:
Switch + Case Block
Switch + case expression
Switch Try + New Catch + enhanced Case block
Switch Try + New Catch + old Case block
Switch try + New Catch expression + case expression
Old Try (with resources)? + Old Catch
Old Try (with resources)? + New catch
<<IN FUTURE>>
Try monad + new catch
Switch  + Try Monad  + new catch + enhanced switch

The right option can be used in different contexts based on use case and
readability.

Final Note:
----------
In general this syntax allows me to do better code reviews "Hey, have all
the exceptional scenarios been handled? (checks the catch arms) Done? Good.
Now let's go to the case arms". (This is similar in spirit to checking all
the preconditions related to method parameters at the beginning of methods)
It also brings the catch block into the future, along with the switch - and
preserves the well learnt pattern - where there is a catch, there is a try
above - What is being caught is for what is inside the try.

HTH

Sincerely,
Ram Anvesh
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-comments/attachments/20240712/7116fb3b/attachment.htm>


More information about the amber-spec-comments mailing list