Some thoughts and an idea about Checked Exceptions

John Hendrikx hjohn at xs4all.nl
Sun Dec 3 16:21:47 UTC 2023


How about:

      try {
            // create directory
            try {
                // create file
            }
            catch (FileAlreadyExistsException e) {
                 throw new IllegalStateException("helpful error message 
2", e);
            }
      }
      catch (FileAlreadyExistsException e) {
            throw new IllegalStateException("helpful error message 1", 
e);
      }

Extract them as functions to make it nicer.

More often though I imagine that it either does not matter much to the 
user (the action failed), or in this specific example that the directory 
already existing is not a failure condition.

--John

------ Original Message ------
>From "David Alayachew" <davidalayachew at gmail.com>
To "amber-dev" <amber-dev at openjdk.org>
Date 03/12/2023 16:31:30
Subject Some thoughts and an idea about Checked Exceptions

>Hello Amber Dev Team,
>
>Here are some thoughts I had about Checked Exceptions, plus an idea.
>
>I actually like Checked Exceptions. I think that, when used correctly, 
>they enable an easy to read style of programming that separates the 
>mess from the happy path.
>
>I think Checked Exceptions are at their best when only one method of a 
>try block can throw a specific exception. Meaning, there is no overlap 
>between the Checked Exceptions of methodA and methodB. This is great 
>because, then, you can wrap all "Throwable" methods in a single try 
>block, and then each catch has a 1-to-1 mapping with the code that can 
>throw it.
>
>Conversely, Checked Exceptions are at their most inconvenient when 
>multiple, consecutive methods can throw the same Checked Exceptions, 
>AND WE WANT TO HANDLE THOSE SAME EXCEPTIONS DIFFERENTLY ACROSS THESE 
>CONSECUTIVE METHODS. In this case, your only real recourse is to handle 
>each task individually with a separate try catch block.
>
>For example - let's say I want to make a new folder, create a file in 
>that folder, and then write content to the newly created file. That 
>seems like a reasonable amount of work for a single method.
>
>For creating the new folder, we have Files.createDirectories() [1]. It 
>throws the Checked Exception FileAlreadyExistsException.
>
>For creating the file and writing content to the newly created file, we 
>have Files.write() [2]. It too throws FileAlreadyExistsException.
>
>Now, what if I want to handle the exceptions differently? The simplest 
>use case would be -- to throw a better error message to the user.
>
>```java
>private Path save(Path parentFolder, byte[] contentToWrite)
>{
>
>	try
>	{
>		Files.createDirectories(...);
>	}
>
>	catch (FileAlreadyExistsException e)
>	{
>		throw new IllegalStateException("helpful error message 1", e);
>	}
>
>	try
>	{
>		return Files.write(...);
>	}
>
>	catch (FileAlreadyExistsException e)
>	{
>		throw new IllegalStateException("helpful error message 2", e);
>	}
>
>}
>```
>
>As a side observation, statement lambdas vs expression lambdas have 
>given me this mental model that blocks are for multiple lines of code 
>while expressions are for one. I know several discussions have been had 
>on try-expressions and whatnot, and I agree that they aren't a good 
>fit. Regardless, having a single method in the try block makes me feel 
>like the noise-to-value ratio is a little high. I can sort of accept it 
>for the catch block, but for try? Annoying.
>
>The side observation is relevant, but going back to the main point -- 
>because I want to handle both cases differently, I must make 2 try 
>catch blocks. I think this is at least one of the reasons why some 
>developers dislike Checked Exceptions.
>
>Now, the obvious solution is to remove the ambiguity, one way or 
>another. There are a couple of ways to do this.
>
>One way is to create a wrapper method that catches and throws a more 
>specific checked exception. Instead of Files.createDirectories(), I 
>create my own Utils.createDirectories() that throws 
>CantCreateDirectoryBecauseFileAlreadyExistsException. Then, I can just 
>catch that specific exception and handle it as expected.
>
>But this means writing a whole bunch of utility style methods to work 
>around a lack of specificity that can only be achieved by wrapping 
>individual lines of code in blocks. I will hereby call them 
>micro-blocks. Ignoring the fact that the utility methods just clog up 
>my codebase, they also tend to be easy to misplace or I accidentally 
>make duplicates of them without meaning to. In short, its a whole bunch 
>of low-value code that is easy to forget and only exists to avoid some 
>friction.
>
>There are a few other ways, but they involve either writing something 
>resembling micro-blocks, or more indirection, like with the utility 
>methods.
>
>Here's my pie-in-the-sky idea. I don't care about syntax. But for now, 
>I will call it Tagged Statements and Tagged Exceptions.
>
>```java
>private Path save(Path parentFolder, byte[] contentToWrite)
>{
>
>	try
>	{
>		#folder Files.createDirectories(...);
>		#file   return Files.write(...);
>	}
>	catch (#folder FileAlreadyExistsException e)
>	{
>		throw new IllegalStateException("helpful error message 1", e);
>	}
>	catch (#file   FileAlreadyExistsException e)
>	{
>		throw new IllegalStateException("helpful error message 2", e);
>	}
>
>}
>```
>
>Doing it this way, all ambiguity is gone, while boiling things down to 
>only the code that needs to be there. Plus, this also gives us the 
>benefit of using the code we have (already written).
>
>The semantics are simple.
>
>* All statements in a method body can be prefixed by a tag -- called a 
>tagged statement.
>
>     * #, followed by an identifier, followed by whitespace, followed by 
>the statement to be tagged.
>
>* All exceptions thrown by the tagged statement can be referenced in 
>catch parameters via a tagged Exception -- an ExceptionType prefixed by 
>the same # identifier.
>
>     * #, followed by an identifier, followed by whitespace, followed by 
>the ExceptionType to be tagged.
>
>* You can't put the # identifier in the middle of a statement 
>(System.out.println(#1 someMethod()) <---- invalid).
>
>And the best part is, this blends in nicely with existing semantics. If 
>you have a catch block with no tagged catch parameters, then it works 
>the way that it always has. But if you want to specify, then use a 
>tagged exception. If you want to handle multiple types of exceptions 
>using the "|" symbol, that logic works exactly for tagged exceptions 
>too. You can even mix and match them. Again, I don't care about syntax. 
>I care about the fact that this is something you can do at the call 
>site ad-hoc.
>
>The part that I like the most about this is that it actually makes 
>try-catch way more attractive. Obviously, if I am trying to do control 
>flow, then try catch is still not the right vehicle (and if I still 
>must, then it should really be handled in its own try catch block or a 
>separate method). But now, all those errors that I didn't really want 
>to specify or build around becomes really easy to do. I just add an 
>inline signifier, then a matching catch block. The only hit to 
>readability is the prefix. You can make it verbose if you like 
>(#recoverable) or terse (#1).
>
>As a potential bonus, it might be a good idea to allow several 
>different statements to have the same # prefix. Meaning, methodA, 
>methodC, and methodE all have #1, but methodB and methodD have #2. I am 
>indifferent to this, and I am fine leaving it out.
>
>Another benefit is that it allows you to handle all Exceptions from 
>that particular join point the same. Let's say there is a method call 
>in your method that all failures it has can be handled the same. Simply 
>attach a prefix to it (#blah) and then make a catch (#blah Exception e) 
>or something similar.
>
>I would also add a warning if a method has a tagged statement that is 
>not explicitly referenced by a catch block. Catch parameters must spell 
>out the tag explicitly to count as an explicit reference.
>
>Now, this solution doesn't solve the "bigger" problems (some would say) 
>with Checked Exceptions (Streams/Lambdas + Checked Exceptions). But I 
>think it makes it makes Checked Exceptions and try catch blocks (both 
>good things that we should be making better use of) extremely ergonomic 
>and easy to handle.
>
>Thoughts?
>
>Thank you for your time!
>David Alayachew
>
>[1]=https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html#createDirectories(java.nio.file.Path,java.nio.file.attribute.FileAttribute...)
>[2]=https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html#write(java.nio.file.Path,byte%5B%5D,java.nio.file.OpenOption...)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20231203/252e5587/attachment.htm>


More information about the amber-dev mailing list