Java lambdas and exception handling

Following on from our introduction to lambdas in Java, we turn to the issue of throwing exceptions from lambda expressions.

Recall that Java exceptions can be checked or unchecked. Checked exceptions are declared in the throws clause of a method. The Java compiler forces such exceptions to be explicitly caught by the caller of that method. Unchecked exceptions, on the other hand, need not be declared and caught: if uncaught, they will simply be "passed up the chain" until they are eventually handled, either by a caller further up the call stack or by the uncaught exception handler. Common examples of unchecked exceptions are NullPointerException and IllegalArgumentException; other examples are any subclass of RuntimeException.

The rules for when you can throw an exception from a lambda are effectively the same as for when you can throw an exception from a method:

As we will see, the general-purpose interfaces in the java.util.function (such as Function, Consumer etc) generally do not allow checked exceptions to be thrown. The practical consequence of this that the with the Java Stream API, workarounds are required when checked exceptions are involved.

Example where we can throw a checked exception from a lambda expression: Callable

The Callable interface, introduced in Java 5, is similar to Runnable. It declares a single call() method, which is declared as throwing Exception, allowing any checked exception to be thrown:

	
public interface Callable<V> {

  V call() throws Exception;
  
}

Therefore, if we define a Callable using a lambda expression, that lambda expression can throw a checked exception (or call a method that throws a checked exception). This example calls Files.readAllLines(), which can throw IOException:

	
Callable<String[]> configFetcher = () -> Files.readAllLines(configPath);

In this example, the lambda code explicitly throws a TimeoutException:

	
Callable<Integer> threadsafeCalc = () -> {
  if (!lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
    throw new TimeoutException("Could not acquire lock in time");
  }

  try {
    return calculateValue0();
  } finally {
    lock.unlock();
  }
};

Examples where the lamda expression cannot throw a checked exception: Function and Consumer

Now we turn to the case where a checked exception cannot be thrown from within a lambda expression. The Function interface, like most of the general-purpose functional interfaces in the java.util.function package, has a method which is not declared as throwing any exceptions:

	
public interface Function<T, R> {

  R apply(T t);
  
}

The following is therefore not possible:

	
// Fails to compile:
Function<Path, String[]> lineReader = (path) -> Files.readAllLines(path);

For similar reasons, the following is not possible:

	
List<Path> pathsToDelete = ....
// Fails to compile:
pathsToDelete.forEach(Files::deleteIfExists);

Here, forEach takes a lConsumer. And the method Consumer.accept(), is not declared as throwing any exceptions. Therefore, we cannot simply call Files.deleteIfExists(), which throws a checked exception (IOException).

We therefore require a workaround for these cases when we need to throw an exception from a lambda expression, but the corresponding functional interface does not allow us to do so. One way or another, we must explicitly catch and deal with the exception so that no checked exception is thrown from the lambda exprssion itself. We essentially have two choices:

To "re-cast" the exception, we catch it and then throw it wrapped in a RuntimeException:

	
pathsToDelete.forEach(path -> {
  try {
    Files.deleteIfExists(path);
  } catch (IOException ioex) {
    throw new RuntimeException("Unexpected I/O error while deleting " + f, ioex);
  }
})

Here we simply use a generic RuntimeException. Because IOException is a common type of exception, a corresponding UnchckedIOException class also exists, and we could use that instead if it was important to distinguish between the error being an IOExeption vs some other type of exception. We could also define our own exception class: so long as it subclasses RuntimeException, we can throw it from a lambda expression and it will still be compatible with the Stream API.

If you have many cases like this where you need to handle checked exceptions inside lambdas, it may be worth creating a utility method to provide a little syntax sugar. For example, by declaring the following once:

	
  private interface ConsumerWithError<T> {
    void accept(T val) throws Exception;
  }

  public static <T> Consumer<T> withError(ConsumerWithError<T> cons) {
    return (val) -> {
      try {
         cons.accept(val);
      } catch (Exception e) {
        throw new RuntimeException("Unexpected exception", e);
      }
    };
  }
we can then write:
	
paths.forEach(withError(Files::deleteIfExists));

Note: an exception halts the Stream pipeline

If we do opt for the latter approach, then we need to bear in mind that if the exception is thrown, it will halt stream processing: in other words, a failure to delete one file in this case will stop subsequent files from being deleted. This is also the case with any other unchecked exception that might be thrown from a labmda expression within the steam, whether explicitly or not: these include NullPointerException, IllegalArgumentException, NumberFormatException...


If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.

Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.