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