3.8. Dealing with Exceptions
When you write a method that accepts lambdas, you need to spend some thought on handling and reporting exceptions that may occur when the lambda expression is executed.
When an exception is thrown in a lambda expression, it is propagated to the caller. There is nothing special about executing lambda expressions, of course. They are simply method calls on some object that implements a functional interface. Often it is appropriate to let the expression bubble up to the caller.
Consider, for example:
public static void doInOrder(Runnable first, Runnable second) { first.run(); second.run(); }
If first.run() throws an exception, then the doInOrder method is terminated, second is never run, and the caller gets to deal with the exception.
But now suppose we execute the tasks asynchronously.
public static void doInOrderAsync(Runnable first, Runnable second) { Thread t = new Thread() { public void run() { first.run(); second.run(); } }; t.start(); }
If first.run() throws an exception, the thread is terminated, and second is never run. However, the doInOrderAsync returns right away and does the work in a separate thread, so it is not possible to have the method rethrow the exception. In this situation, it is a good idea to supply a handler:
public static void doInOrderAsync(Runnable first, Runnable second, Consumer<Throwable> handler) { Thread t = new Thread() { public void run() { try { first.run(); second.run(); } catch (Throwable t) { handler.accept(t); } } }; t.start(); }
Now suppose that first produces a result that is consumed by second. We can still use the handler.
public static <T> void doInOrderAsync(Supplier<T> first, Consumer<T> second, Consumer<Throwable> handler) { Thread t = new Thread() { public void run() { try { T result = first.get(); second.accept(result); } catch (Throwable t) { handler.accept(t); } } }; t.start(); }
Alternatively, we could make second a BiConsumer<T, Throwable> and have it deal with the exception from first—see Exercise 16.
It is often inconvenient that methods in functional interfaces don’t allow checked exceptions. Of course, your methods can accept functional interfaces whose methods allow checked exceptions, such as Callable<T> instead of Supplier<T>. A Callable<T> has a method that is declared as T call() throws Exception. If you want an equivalent for a Consumer or a Function, you have to create it yourself.
You sometimes see suggestions to “fix” this problem with a generic wrapper, like this:
public static <T> Supplier<T> unchecked(Callable<T> f) { return () -> { try { return f.call(); } catch (Exception e) { throw new RuntimeException(e); } catch (Throwable t) { throw t; } }; }
Then you can pass a
unchecked(() -> new String(Files.readAllBytes( Paths.get("/etc/passwd")), StandardCharsets.UTF_8))
to a Supplier<String>, even though the readAllBytes method throws an IOException.
That is a solution, but not a complete fix. For example, this method cannot generate a Consumer<T> or a Function<T, U>. You would need to implement a variation of unchecked for each functional interface.