DEV Community

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

Posted on

3 1

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)

Great read:

Is it Time to go Back to the Monolith?

History repeats itself. Everything old is new again and I’ve been around long enough to see ideas discarded, rediscovered and return triumphantly to overtake the fad. In recent years SQL has made a tremendous comeback from the dead. We love relational databases all over again. I think the Monolith will have its space odyssey moment again. Microservices and serverless are trends pushed by the cloud vendors, designed to sell us more cloud computing resources.

Microservices make very little sense financially for most use cases. Yes, they can ramp down. But when they scale up, they pay the costs in dividends. The increased observability costs alone line the pockets of the “big cloud” vendors.

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay