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