Proposal: Operator to "demote" checked exceptions

negora public at negora.com
Mon Mar 7 11:07:35 UTC 2022


Hi:

In their current form, many Java APIs make it impossible to throw 
checked exceptions when you implement their abstract methods (i.e. the 
interfaces in `java.util.function`). And, even if they allowed it, we 
would need a way to specify a variable number of exceptions, not just a 
fixed one.

Since it's very unlikely that this feature is ever supported (I wish to 
be wrong), I think it's better to be pragmatic and look for a way to 
turn checked exceptions off (demote them) in those layers where they're 
unsupported, and then be able to turn them on again (promote them).

Nowadays, in these situations, we're forced to wrap checked exceptions 
with unchecked ones, which has several disadvantages:

1. It adds a lot of noise to the code (lots of try-catch blocks).

2. The unchecked exception is just a mere wrapper, so people tend to 
reuse a single unchecked exception whose name usually is not meaningful 
in the current context.

3. If you want to rethrow the original exceptions, you're forced to 
catch the wrapper first, and then analyse and downcast the wrapped 
exceptions.

4. Other times, people is lazy and doesn't wrap the exceptions at all, 
leaving the `catch` block empty.


## Proposal.

I propose an operator to demote checked exceptions. By _demoting_ I'm 
referring to making them behave as unchecked exceptions, so that they 
propagate transparently, but keeping their class hierarchy intact (they 
won't have any relation to `RuntimeException`).

This operator would be used in the `throws` clause of the methods, and 
would be prepended to each checked exception that we wish to demote. I 
will use the `~` character as operator in my examples, but that's just 
an idea.

This feature would be something like the `@SneakyThrows` annotation of 
[Project Lombok](https://projectlombok.org/), but more powerful:

1. It would be part of the Java language.

2. It could be used directly in the `throws` clause.

3. It could be used with lambdas too.

For example, imagine a class that implements an `Iterator`, which lazily 
parses students from a file. This is the code (omitting non-important 
parts):

```
public final class StudentIterator
     implements Iterator<Student> {

   private Student student;

   public boolean hasNext ()
       throws ~ParseException,
         ~IOException {
         // ^^^ The operator ~ demotes the checked exceptions,
         // so that they're propagated transparently.
         // It also makes that they are NOT part of the signature
         // of the method.

     if (student == null) {
       student = parseNextStudent ();
     }
     return (student != null)

   }

   private Student parseNextStudent ()
       throws ParseException,
         IOException {
   }

}
```

Now, to catch these exceptions in an upper layer, **Java would need to 
remove the rule that forbids catching a checked exception which is not 
explicitly thrown in the associated `try` block.** This is so because a 
demoted exception couldn't be detected by the compiler, obviously. The 
try-catch would look like always:

```
public void parseAndStoreStudents (Reader reader) {

   try {
     Iterator<Student> students = parseStudents (reader);
     storeStudents (students);
   } catch (ParseException | IOException ex) {
           // ^^^ Nothing special here.
     logAndNotifyByEmail (ex);
   }

}
```

If we wanted to promote the demoted exceptions again, then we would 
simply do this:

```
public void parseAndStoreStudents (Reader reader)
     throws ParseException,
       IOException {
       // ^^^ Nothing special here, either.

   Iterator<Student> students = parseStudents (reader);
   storeStudents (students);

}
```

In case that the method doesn't need to expose the exceptions because 
they're part of the implementation, we would simply let the demoted 
exceptions propagate:

```
public void obtainStudents () {
	Reader reader = getReader ();
   Iterator<Student> students = parseStudents (reader);
   storeStudents (students);
}
```


## Use with lambdas.

Imagine a method that lazily parses students from text lines:

```
public Stream<Student> parseStudents (Reader reader) {
   Stream<String> lines = parseLines (reader);
   return lines.map (line -> parseStudent ());
                             // ^^^ This makes the compilation fail
                             // because the "ParseException" has not
been
                             // caught.
}

public Student parseStudent (String line) {
     throws ParseException {
}
```

Java could introduce a variation of the `->` operator, such as `~>`:

```
public Stream<Student> parseStudents (Reader reader) {
   Stream<String> lines = parseLines (reader);
   return lines.map (line ~> parseStudent ());
}
```

This would be equivalent to the following:

```
public Stream<Student> parseStudents (Reader reader) {
   Stream<String> lines = parseLines (reader);
   return lines.map (
     new Function<> () {
       @Override
       public Student apply (String line)
           throws ~ParseException {
         return parseStudent (line);
       }
     }
   );
```


## May programmers abuse this feature?

People who hate checked exceptions might be tempted to demote every 
checked exception, that's true. But they are already doing that without 
this operator: they create utility classes and wrapper classes only for 
that purpose. Even specifications, such as JPA, or libraries, such as 
Hibernate or NIO, decided to use mostly or only unchecked exceptions.

However, I believe that **this operator is an opportunity to encourage 
again the use of checked exceptions in our APIs** (where appropriate, of 
course), without the fear of causing a hell of try-catches to our users 
or to ourselves (if we write full applications).

Those who find checked exceptions valuable (like me) will use them like 
always have done, but will count with a tool to:

1. Demote checked exceptions temporarily in those layers where they 
don't fit well (`Stream` I'm looking at you) and promote them again 
right after that.

2. Demote checked exceptions permanently where we don't want them to be 
exposed.


More information about the amber-dev mailing list