Exception Handling in Java Streams
When you want to use a method that throws a checkedException, you have to do something extra if you want to call it in a lambda.
Join the DZone community and get the full member experience.
Join For FreeThe Stream API and lambda’s where a big improvement in Java since version 8. From that point on, we could work with a more functional syntax style. Now, after a few years of working with these code constructions, one of the bigger issues that remain is how to deal with checked exceptions inside a lambda.
As you all probably know, it is not possible to call a method that throws a checked exception from a lambda directly. In some way, we need to catch the exception to make the code compile. Naturally, we can do a simple try-catch inside the lambda and wrap the exception into a RuntimeException
, as shown in the first example, but I think we can all agree that this is not the best way to go.
myList.stream()
.map(item -> {
try {
return doSomething(item);
} catch (MyException e) {
throw new RuntimeException(e);
}
})
.forEach(System.out::println);
Most of us are aware that block lambdas are clunky and less readable. They should be avoided as much as possible, in my opinion. If we need to do more than a single line, we can extract the function body into a separate method and simply call the new method. A better and more readable way to solve this problem is to wrap the call in a plain old method that does the try-catch and call that method from within your lambda.
myList.stream()
.map(this::trySomething)
.forEach(System.out::println);
private Item trySomething(Item item) {
try {
return doSomething(item);
} catch (MyException e) {
throw new RuntimeException(e);
}
}
This solution is at least a bit more readable and we do separate our concerns. If you really want to catch the exception and do something specific and not simply wrap the exception into a RuntimeException
, this can be a possible and readable solution for you.
The Complete Java Developer Course.*
*Affiliate link. See Terms of Use.
RuntimeException
In many cases, you will see that people use these kinds of solutions to repack the exception into a RuntimeException
or a more specific implementation of an unchecked Exception. By doing so, the method can be called inside a lambda and be used in higher order functions.
I can relate a bit to this practice because I personally do not see much value in checked exceptions in general, but that is a whole other discussion that I am not going to start here. If you want to wrap every call in a lambda that has a checked into a RuntimeException
, you will see that you repeat the same pattern. To avoid rewriting the same code over and over again, why not abstract it into a utility function? This way, you only have to write it once and call it every time you need it.
To do so, you first need to write your own version of the functional interface for a function. Only this time, you need to define that the function may throw an exception.
@FunctionalInterface
public interface CheckedFunction<T,R> {
R apply(T t) throws Exception;
}
Now, you are ready to write your own general utility function that accepts a CheckedFunction
as you just described in the interface.You can handle the try-catch in this utility function and wrap the original exception into a RuntimeException
(or some other unchecked variant). I know that we now end up with an ugly block lambda here and you could abstract the body from this. Choose for yourself if that is worth the effort for this single utility.
public static <T,R> Function<T,R> wrap(CheckedFunction<T,R> checkedFunction) {
return t -> {
try {
return checkedFunction.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
With a simple static import, you can now wrap the lambda that may throw an exception with your brand new utility function. From this point on, everything will work again.
myList.stream()
.map(wrap(item -> doSomething(item)))
.forEach(System.out::println);
The only problem left is that when an exception occurs, the processing of the your stream stops immediately. If that is no problem for you, then go for it. I can imagine, however, that direct termination is not ideal in many situations.
Either
When working with streams, we probably don't want to stop processing the stream if an exception occurs. If your stream contains a very large amount of items that need to be processed, do you want that stream to terminate when for instance the second item throws an exception? Probably not.
Let's turn our way of thinking around. Why not consider "the exceptional situation" just as much as a possible result as we would for a "successful" result. Let's consider it both as data, continuing to process the stream, and decide afterward what to do with it. We can do that, but to make it possible, we need to introduce a new type — the Either type.
The Either type is a common type in functional languages and not (yet) part of Java. Similar to the Optional type in Java, an Either
is a generic wrapper with two possibilities. It can either be a Left or a Right but never both. Both left and right can be of any types. For instance, if we have an Either value, this value can either hold something of type String or of type Integer, Either<String,Integer>
.
If we use this principle for exception handling, we can say that our Either
type holds either an Exception
or a value. By convenience, normally, the left is the Exception and the right is the successful value. You can remember this by thinking of the right as not only the right-hand side but also as a synonym for “good,” “ok,” etc.
Below, you will see a basic implementation of the Either
type. In this case, I used the Optional
type when we try to get the left or the right because we:
public class Either<L, R> {
private final L left;
private final R right;
private Either(L left, R right) {
this.left = left;
this.right = right;
}
public static <L,R> Either<L,R> Left( L value) {
return new Either(value, null);
}
public static <L,R> Either<L,R> Right( R value) {
return new Either(null, value);
}
public Optional<L> getLeft() {
return Optional.ofNullable(left);
}
public Optional<R> getRight() {
return Optional.ofNullable(right);
}
public boolean isLeft() {
return left != null;
}
public boolean isRight() {
return right != null;
}
public <T> Optional<T> mapLeft(Function<? super L, T> mapper) {
if (isLeft()) {
return Optional.of(mapper.apply(left));
}
return Optional.empty();
}
public <T> Optional<T> mapRight(Function<? super R, T> mapper) {
if (isRight()) {
return Optional.of(mapper.apply(right));
}
return Optional.empty();
}
public String toString() {
if (isLeft()) {
return "Left(" + left +")";
}
return "Right(" + right +")";
}
}
You can now make your own functions return an Either
instead of throwing an Exception
. But that doesn't help you if you want to use existing methods that throw a checked Exception
inside a lambda right? Therefore, we have to add a tiny utility function to the Either
type I described above.
public static <T,R> Function<T, Either> lift(CheckedFunction<T,R> function) {
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception ex) {
return Either.Left(ex);
}
};
}
By adding this static lift method to the Either
, we can now simply "lift" a function that throws a checked exception and let it return an Either
. If we take the original problem, we now end up with a Stream of Eithers instead of a possible RuntimeException
that may blow up my entire Stream
.
myList.stream()
.map(Either.lift(item -> doSomething(item)))
.forEach(System.out::println);
This simply means that we have taken back control. By using the filter function in the Stream APU, we can simply filter out the left instances and, for example, log them. You can also filter the right instances and simply ignore the exceptional cases. Either way, you are back in control again and your stream will not terminate instantly when a possible RuntimeException
occurs.
Because Either
is a generic wrapper, it can be used for any type, not just for exception handling. This gives us the opportunity to do more than just wrapping the Exception
into the left part of an Either
. The issue we now might have is that if the Either
only holds the wrapped exception, and we cannot do a retry because we lost the original value. By using the ability of the Either
to hold anything, we can store both the exception and the value inside a left. To do so, we simply make a second static lift function like this.
public static <T,R> Function<T, Either> liftWithValue(CheckedFunction<T,R> function) {
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception ex) {
return Either.Left(Pair.of(ex,t));
}
};
}
You see that in this liftWithValue
function within the Pair
type is used to pair both the exception and the original value into the left of an Either.
Now, we have all the information we possibly need if something goes wrong, instead of only having the Exception
.
The Pair
type used here is another generic type that can be found in the Apache Commons lang library, or you can simply implement your own. Anyway, it is just a type that can hold two values.
public class Pair<F,S> {
public final F fst;
public final S snd;
private Pair(F fst, S snd) {
this.fst = fst;
this.snd = snd;
}
public static <F,S> Pair<F,S> of(F fst, S snd) {
return new Pair<>(fst,snd);
}
}
With the use of the liftWithValue
, you now have all the flexibility and control to use methods that may throw an Exception
inside a lambda. When the Either
is a right, we know that the function was applied correctly and we can extract the result. If, on the other hand, the Either
is a left, we know something went wrong and we can extract both the Exception
and the original value, so we can proceed as we like. By using the Either
type instead of wrapping the checked Exception
into a RuntimeException
, we prevent the Stream
from terminating halfway.
Try
People that may have worked with for instance Scala may use the Try
instead of the Either
for exception handling. The Try
type is something that is very similar to the Either
type. It has, again, two cases: “success” or “failure.” The failure can only hold the type Exception
, while the success can hold anything type you want. So, the Try
is nothing more than a specific implementation of the Either
where the left type (the failure) is fixed to type Exception
.
public class Try<Exception, R> {
private final Exception failure;
private final R succes;
public Try(Exception failure, R succes) {
this.failure = failure;
this.succes = succes;
}
}
Some people are convinced that it is easier to use, but I think that because we can only hold the Exception
itself in the failure part, we have the same problem as explained in the first part of the Either
section. I personally like the flexibility of the Either
type more. Anyway, in both cases, if you use the Try
or the Either
, you solve the initial problem of exception handling and do not let your stream terminate because of a RuntimeException
.
Libraries
Both the Either
and the Try
are very easy to implement yourself. On the other hand, you can also take a look at functional libraries that are available. For instance, VAVR (formerly known as Javaslang) does have implementations for both types and helper functions available. I do advise you to take a look at it because it holds a lot more than only these two types. However, you have ask yourself the question of whether you want this large library as a dependency just for exception handling when you can implement it yourself with just a few lines of code.
Conclusion
When you want to use a method that throws a checkedException
, you have to do something extra if you want to call it in a lambda. Wrapping it into a RuntimeException
can be a solution to make it work. If you prefer to use this method, I urge you to create a simple wrapper tool and reuse it, so you are not bothered by the try/catch
every time.
If you want to have more control, you can use the Either
or Try
types to wrap the outcome of the function, so you can handle it as a piece of data. The stream will not terminate when a RuntimeException
is thrown and you have the liberty to handle the data inside your stream as you please.
If you enjoyed this article and want to learn more about Java Streams, check out this collection of tutorials and articles on all things Java Streams.
Opinions expressed by DZone contributors are their own.
Comments