DEV Community

Cover image for Handling Exceptions in Java Streams using a Functional Interface
Perry H
Perry H

Posted on

Handling Exceptions in Java Streams using a Functional Interface

I was reviewing some code recently and ran into something like this:

subject.getIdentities().forEach(i -> { try { caseService.updateDocument(i.getCase()); } catch (Exception e) { log.error(e); } }); 
Enter fullscreen mode Exit fullscreen mode

If you have read my other articles about functional programming concepts in Java (here and here), you can probably guess that I am a big fan of lambda expressions. However, one of the primary advantages of using lambda expressions is terseness. The above code looks not-so-terse and a bit messy to me. How can we clean this up? Functional interfaces to the rescue.

What do we need? We know that a forEach expects a Consumer as an input. If we could wrap our logic that handles an exception in a Consumer, we could use the logic in the forEach.

The main logic inside the forEach is the following line:

//try catch removed // i is an Identity and updateDocument returns a UpdateResponse i -> caseService.updateDocument(i.getCase()); 
Enter fullscreen mode Exit fullscreen mode

We know the input and the return type, and we can create a functional interface whose method throws an Exception.

@FunctionalInterface public interface ThrowingFunction<Identity, UpdateResponse> { UpdateResponse apply(Identity i) throws Exception; } 
Enter fullscreen mode Exit fullscreen mode

We can make this more usable with generics.

@FunctionalInterface public interface ThrowingFunction<T, R> { R apply(T t) throws Exception; } 
Enter fullscreen mode Exit fullscreen mode

With the interface created, the original logic can be target typed as such:

ThrowingFunction<Identity, UpdateResponse> tf = i -> caseService.updateDocument(i.getCase()); 
Enter fullscreen mode Exit fullscreen mode

Now that we have a functional interface for our logic, we can pass it as a parameter to a method that handles the exception and returns a Consumer we can use in the forEach.

private static <T, R> Consumer<T> wrap(ThrowingFunction<T, R> f) { return t -> { try { f.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } 
Enter fullscreen mode Exit fullscreen mode

It's a little weird looking, but essentially the wrap method takes a ThrowingFunction as input and handles executing the function or catching and throwing the Exception in a Consumer.

Now we can wrap any logic used inside a forEach that may throw an exception. It looks something like this:

// target type the original logic ThrowingFunction<Identity, UpdateResponse> tf = i -> caseService.updateDocument(i.getCase()): // pass logic to the wrapmethod // which will execute the function or throw a RunTimeException.  Consumer<Identity> p = wrap(tf); // use Consumer in foreach subject.getIdentities().forEach(p); 
Enter fullscreen mode Exit fullscreen mode

Or if you prefer one line:

subject.getIdentities().forEach(wrap(i -> caseService.updateDocument(i.getCase()))); 
Enter fullscreen mode Exit fullscreen mode

Much better! You could implement something similar to handle different types of functional interfaces. For instance, a map operation only takes a Function as an input. Instead of a wrap method that returns a Consumer you could have a method that returns a Function.
This is just one pattern for handling exceptions in streams. I should mention there are libraries that do this kind of thing for you if you don't want to roll with your own implementation. Or you could use a monad to handle success/failures, but that is beyond the scope of this post. Let me know if you have implemented any other ways of handling exceptions in streams!

Top comments (0)