Better Checked Exception Handing… If Java Had Default Generics

I believe that, if done right, checked exceptions can benefit an API. However, they weren’t given the proper TLC in the new Java 8 APIs, particularly in the Streams. But if Java was enhanced with default generics, the mess could be cleaned up.

The problem

Java 8 introduced a plethora of functional interfaces, but none of the methods throw exceptions. That’s fine if you’re only throwing extensions of RuntimeExcption, but it becomes extremely painful when dealing with checked exceptions. For example, consider the following:

public class Example {
    public void example(Stream<MyType> stream) {
        stream
            .filter(myType::isAvailable)
            .map(myType::process)
            .forEach(this::writeToFile);
    }

    private void writeToFile(String value) throws IOException {
        // implementation details...
    }

    // Assume both exceptions are checked exceptions.
    public interface MyType {
        boolean isAvailable() throws AvailabilityException;
        Object process() throws ProcessException;
    }
}

This will fail to compile, miserably. This is what has to be done to fix it:

public void example(Stream<MyType> stream) {
    stream
        .filter(myType -> {
            try {
                return myType.isAvailable();
            } catch (AvailabilityException e) {
                // TODO What can we do here?
            }
        })
        .map(myType -> {
            try {
                return myType.process();
            } catch (ProcessException e) {
                // TODO What can we do here?
            }
        })
        .forEach(myType -> {
            try {
                Example.this.writeToFile(myType);
            } catch (IOException e) {
                // TODO No, seriously, what can we do here?
            }
        });
}

Terrible, isn’t it? What do we do inside all of the catch blocks? Each is probably too low level to properly handle the exception, so the best we can do—assuming we cannot rewrite the checked exceptions into runtime exceptions—is wrap them in a RuntimeException, rethrow it, and put a try/catch around the stream call chain:

public void example(Stream<MyType> stream) {
    try {
        stream
            .filter(myType -> {
                try {
                    return myType.isAvailable();
                } catch (AvailabilityException e) {
                    throw new RuntimeException(e);
                }
            })
            .map(myType -> {
                try {
                    return myType.process();
                } catch (ProcessException e) {
                    throw new RuntimeException(e);
                }
            })
            .forEach(myType -> {
                try {
                    Example.this.writeToFile(myType);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
    } catch (RuntimeException e) {
        Exception cause = e.getCause();
        if (e instanceof AvailabilityException) {
            AvailabilityException real
                = (AvailabilityException) cause;
            // properly handle the exception
        } else if (e instanceof ProcessException) {
            ProcessException real
                = (ProcessException) cause;
            // properly handle the exception
        } else if (e instanceof IOException) {
            IOException real
                = (IOException) cause;
            // properly handle the exception
        } else {
            throw e;
        }
    }
}

This looks even worse; there must be a better way!

Default Generic Type

What if Function where changed to this:

@FunctionalInterface
public interface Function<T, R, E extends Exception> {
    R apply(T t) throws E;
    // ...helper methods omitted...
}

First, it would break backwards compatibility (the “omitted helper methods” would be trickier to implement), and all existing implementations would have to be rewritten so that E was defined as RuntimeException.

But this might be avoided if default generics were added to the language:

@FunctionalInterface
public interface Function<T, R,
        E extends Exception default RuntimeException> {
    R apply(T t) throws E;
    // ...helper method omitted...
}

Likewise, many of the Java 8 APIs could be rewritten to account for this. For example, the Stream#map function:

public interface Stream<T> extends BaseStream<T, Stream<T>> {
    // ...omitted...
    <R, E extends Exception default RuntimeException> 
    Stream<R> map(
            Function<? super T, ? extends R, E> mapper) throws E;
    // ...omitted...
}

Like Java 8’s default methods, default generics could provide a means of API evolution.

Putting it Together

How does this help? It allows checked exceptions to pop out of the Stream methods like map, filter, reduce in a much more elegant way. Our previous example might look something like this:

public void example(Stream<MyType> stream) throws IOException {
    try {
        stream
            .filter(myType::isAvailable)
            .map(myType::process)
            .forEach(this::writeToFile);
    } catch (AvailabilityException e) {
        // properly handle the exception
    } catch (ProcessException e) {
        // properly handle the exception
    }
}

And isn’t this what is should look like?

The Fine Print

  • AFAIK, default generics is in no way being considered for inclusion into Java. Call me a dreamer.
  • Adding default generics to Java would be a rather large change to the language. While it solves the irritating case above, there are undoubtedly unforeseen consequences I have not considered.
    • There can be more than one exception in a throws list, but generics are limited to one. This makes generic exceptions tricky, as discussed more here.
  • I’m not qualified to answer how this would affect backwards compatibility.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s