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!

Discussion (0)