Some thoughts and an idea about Checked Exceptions

David Alayachew davidalayachew at gmail.com
Sun Dec 3 15:31:30 UTC 2023


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/4c64fe33/attachment-0001.htm>


More information about the amber-dev mailing list