From ramanvesh at gmail.com Fri Jul 12 18:50:47 2024 From: ramanvesh at gmail.com (ram anvesh reddy) Date: Fri, 12 Jul 2024 18:50:47 -0000 Subject: Exception handling in switch Message-ID: 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: <> 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 <> 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: