Tunneling Checked Java Exceptions Through Lambdas

Categories: Java

Introduction

I wrote an article promoting the use of checked exceptions in Java - or at least defending checked exceptions against the argument that “Java checked exceptions are bad”.

There is one use-case where checked exceptions are particularly ugly to handle: when using lambdas (anonymous functions). For some reason, the designers of the JDK classes defined a set of types to represent lambdas which do not throw exceptions (eg Supplier, Function) and then defined many standard-library methods which accept these types. This means those utility methods cannot directly be invoked with lambdas which throw exceptions. One example is Stream.map(Function).

The simplest solution is to just disable checked exceptions, either by rewriting functions to use unchecked exceptions or by using the Sneaky Throws approach. However as I argue in the article above, that’s a bad solution which leads to error-prone applications.

The best solution is probably to simply write lambdas which do not throw exceptions, instead returning some type that can represent failures as data (eg the traditional functional-programming type Either). Sadly, no such type has been provided in the JDK. However there are many open-source projects which address this problem, eg vavr.

If you cannot or do not wish to make that jump, there are a number of libraries which directly solve the lambda problem by “tunneling” the checked exception in a more controlled way than SneakyThrows (see section “Alternatives” below). Sadly, I found many of them to be poorly documented - ie it wasn’t clear to me how to use them.

I’ve decided to add to the set of options by creating my own “tunneling” solution. It consists of just one class, so is easy to simply copy into a project (a library would be overkill). I haven’t used this class extensively yet, ie can’t guarantee that it works well in real world situations. There may also be some code-structures that it doesn’t support. However its basic functionality seems elegant to me. It is also more type-safe than many of the other proposals.

This solution allows suppressing specific checked exceptions - but only within the scope of a specific try-catch clause. The compiler therefore forces all checked exceptions to be handled (as recommended by my earlier article), but that handling can be done in a try/catch clause that encloses multiple lambdas.

The amount of extra code needed is minimal, and IMO clear to read.

Here’s an example of code using it:

    public void run() {
        List<String> strings = List.of("one", "two", "three");

        try (var t1 = Tunnel.of(IOException.class); var t2 = Tunnel.of(InterruptedException.class)) {
            // Object t1 can be used for suppressing exceptions from fnThrowingIOException; t2 cannot.
            // Object t1's close-method forces catching of IOException at end of the try-clause.
            String result1 = strings.stream()
                    .map(t1.tunnel(s -> fnThrowingIOException(s)))
                    .collect(Collectors.joining(","));
            System.out.println(result1);

            // Object t1 can be used for suppressing exceptions from fnThrowingSubtypeOfIOException; t2 cannot.
            // Object t1's close-method forces catching of IOException at end of the try-clause.
            String result2 = strings.stream()
                    .map(t1.tunnel(s -> fnThrowingSubtypeOfIOException(s)))
                    .collect(Collectors.joining(","));
            System.out.println(result2);

            // Object t2 can be used for suppressing exceptions from fnThrowingInterruptedException; t1 cannot.
            // Object t2's close-method forces catching of InterruptedException at end of the try-clause.
            String result3 = strings.stream()
                    .map(t2.tunnel(s -> fnThrowingInterruptedException(s))
                    ).collect(Collectors.joining(","));
            System.out.println(result3);
        } catch(IOException|InterruptedException e) {
            System.out.println("found:" + e);
        }
    }

And here’s the full implementation (which you are welcome to copy/paste and modify as long as the copyright statement is retained).

// Copyright Simon Kitching (vonos.net) 2021. Licensed under the Apache License 2.0
package net.vonos.tunnel;

import java.util.function.Function;

/**
 * Helper for tunneling checked exceptions from within a Lambda to a higher level in a type-safe way.
 * <p>
 * It is recommended that code represent errors as return-values rather than exceptions where possible (in
 * which case this class is not needed). However if that is not an option, then this class allows disabling of
 * checked exceptions in a controlled manner; checked exceptions do not need to be handled within a lambda,
 * but do need to be handled within an enclosing try-with-resources clause.
 * </p>
 * <p>
 * This class extends the well-known "sneaky throws" pattern, re-adding type-safety. The "sneaky throws"
 * pattern allows the Java compiler's checked-exception-validation mechanisms to be suppressed for a specific
 * method-call (see method "tunnel" below). The AutoCloseable interface is then used to re-enable
 * checked exception validation at a "higher level".
 * </p>
 * <p>
 * In the following example, exception-validation is disabled for someFnThrowingException within the lambda-expression,
 * but the catch-clause at the end of the try-clause is mandatory (required by Java compiler). In other words,
 * checked-exception-handling is disabled within the scope of the try-with-resources, but pops back up again at
 * the end of that scope.
 * </p>
 * <pre>
 * try (var t = Tunnel.of(SomeException.class) {
 *     return somelist.stream().map(t.tunnel(s->someFnThrowingException()).collect(...);
 * } catch(SomeException.class) {
 *     ...
 * }
 * </pre>
 */
public class Tunnel<E extends Exception> implements AutoCloseable {
    public static <E extends Exception> Tunnel<E> of(Class<E> etype) {
        return new Tunnel<E>(etype);
    }

    private final Class<E> etype;

    /**
     * Constructor. The class-param here is not actually used, but provides an elegant and readable way
     * of specifying generic type E for this class.
     */
    Tunnel(Class<E> etype) {
        this.etype = etype;
    }

    /**
     * Suppress validation of checked exceptions thrown by the specified function.
     */
    public <E2 extends E, T, R> Function<T,R> tunnel(ThrowingFunction<E2, T, R> f) {
        return new Wrapper<>(f);
    }

    /**
     * Suppress validation of checked exceptions thrown by the specified function.
     */
    public <E2 extends E, T, R> Function<T,R> tunnel(Class<E2> ex, ThrowingFunction<E2, T, R> f) {
        return new Wrapper<>(f);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Exception, R> R sneakyThrow(Exception t) throws T {
        throw (T) t;
    }

    /**
     * Although this close-method is empty (performs no operation), its signature ensures that the exception
     * which method tunnel suppresses must be caught at the end of a surrounding try-clause.
     */
    @Override
    public void close() throws E {
    }

    private static final class Wrapper<E extends Exception, T, R> implements Function<T, R> {
        private final ThrowingFunction<E, T,R> f;
        private Wrapper(ThrowingFunction<E, T, R> f) {
            this.f = f;
        }

        @Override
        public R apply(T param) {
            try {
                return f.apply(param);
            } catch(Exception e) {
                return sneakyThrow(e);
            }
        }
    }

    @FunctionalInterface
    public interface ThrowingFunction<E extends Exception, T, R> {
        R apply(T t) throws E;
    }
}

Future Extensions

One problem with try/catch blocks is that when multiple lines of code within the block can throw the same exception-type, it is no longer possible to find out which line was the cause - which may limit the options for error-recovery. The usual solution is to break code up into multiple try/catch blocks - but that rather defeats the point of separating error-handling code from the “success path”.

With this Tunnel class, it could be possible to “register” each exception befure invoking sneakyThrow, allowing code in a catch-block to then do:

  if (t1.wasActivated()) {
    // here we know the reason this catch-block was triggered is that something wrapped by t1.tunnel(...) threw an exception
  }

Alternatives

I am aware of the following alternative solutions to this issue: