Some thoughts and an idea about Checked Exceptions
David Alayachew
davidalayachew at gmail.com
Sun Dec 3 18:30:18 UTC 2023
Hello John,
Thank you for your response!
> Extract them as functions to make it nicer.
Yes, this is what I mentioned in my original post. Ultimately, that is the
solution we are at, but my real world code runs into readability problems
the more I rely on that. I am bringing all of this up because I feel like
that is an unbalanced cost for the right answer.
> 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.
Oh this is a super simplified example of a real problem I run into at work.
The most recent example from work involves me calling 12 different services
to fetch data models. There are some failures worth failing on, others we
can try and redeem, and others that are non-issues. Handling each one
individually with a try-catch block takes a lot of effort, and oftentimes,
makes the logic more spread out and difficult to follow. I am not asking
for conciseness, I am saying my current solution is fairly difficult to
read, and I'd like that to change. I am proposing one solution.
On Sun, Dec 3, 2023 at 11:21 AM John Hendrikx <hjohn at xs4all.nl> wrote:
> 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/8c8e02df/attachment.htm>
More information about the amber-dev
mailing list