Course Content
Stream API
Stream API
Exception Handling in Stream API
Handling exceptions in Stream API requires a special approach. Unlike traditional loops, where a try-catch
block can be placed inside the loop body, streams operate declaratively, making exception handling within them more complex.
If an exception is not handled, it interrupts the entire stream processing. In this section, you'll explore the right way to catch and handle exceptions in Stream API.
The Exception Handling Problem
Let's say our online store has a getTotal()
method that may throw an exception if order data is corrupted or missing. For example, an order might be loaded from a database where the total amount is stored as null
.
Now, if any order has a total less than 0, the entire Stream API process will terminate with an exception.
But there's a problem—this code won't even run because you're not handling the exceptions that may occur in the getTotal()
method. So, let's take a look at how you can handle exceptions in Stream API.
Handling Exceptions in Stream API
Since try-catch
cannot be used directly inside lambdas, there are several strategies for handling exceptions in Stream API.
One approach is to catch the exception directly inside map()
and replace it with a processed result:
Inside mapToDouble()
, you catch the exception and throw a RuntimeException
, specifying which user caused the issue. This approach is useful when you need to immediately halt execution and quickly identify the problem.
Skipping Elements with Errors
Sometimes, you don't want to stop the entire process when an error occurs—you just need to skip problematic elements. To achieve this, you can use filter()
with exception handling:
If an error occurs in mapToDouble(Order::getTotal)
, the entire order stream processing would normally stop. However, the try-catch
block inside filter()
prevents this, ensuring that only the problematic user is excluded from the final list.
Exception Handling with a Wrapper
To make our code more robust, you can create a wrapper method that allows handling exceptions inside lambdas while automatically catching them.
Java does not allow a Function<T, R>
to throw checked exceptions. If an exception occurs inside apply()
, you either have to handle it within the method or wrap it in a RuntimeException
, making the code more complex. To simplify this, let's define a custom functional interface:
This interface functions similarly to Function<T, R>
but allows apply()
to throw an exception.
Now, let's create an ExceptionWrapper
class with a wrap()
method that converts a ThrowingFunction<T, R>
into a standard Function<T, R>
and accepts a second parameter specifying the fallback value in case of an exception:
The wrap()
method takes a ThrowingFunction<T, R>
and converts it into a standard Function<T, R>
while handling exceptions. If an error occurs, it logs the message and returns the specified default value.
Using It in Stream API
Let's say you have a list of users in an online store, and you need to find active users who have at least three orders worth more than 10,000. However, if an order has a negative amount, you don't want to stop the stream—you simply return 0 as an indication that the price was invalid.
Main
package com.example; import java.util.List; import java.util.function.Function; public class Main { public static void main(String[] args) { List<User> users = List.of( new User("Alice", true, List.of(new Order(12000.00), new Order(15000.00), new Order(11000.00))), new User("Bob", true, List.of(new Order(8000.00), new Order(9000.00), new Order(12000.00))), new User("Charlie", false, List.of(new Order(15000.00), new Order(16000.00), new Order(17000.00))), new User("David", true, List.of(new Order(5000.00), new Order(20000.00), new Order(30000.00))), new User("Eve", true, List.of(new Order(null), new Order(10000.00), new Order(10000.00), new Order(12000.00))), new User("Frank", true, List.of(new Order(-5000.00), new Order(10000.00))) // Error: Negative order amount ); List<User> premiumUsers = users.stream() .filter(User::isActive) .filter(user -> user.getOrders().stream() .map(ExceptionWrapper.wrap(Order::getTotal, 0.0)) // Using the wrapper function .filter(total -> total >= 10000) .count() >= 3) .toList(); System.out.println("Premium users: " + premiumUsers); } } // The `Order` class represents a customer's order class Order { private final Double total; public Order(Double total) { this.total = total; } public double getTotal() throws Exception { if (total == null || total < 0) { throw new Exception("Error: Order amount cannot be negative or equal to null!"); } return total; } } // The `User` class represents an online store user class User { private final String name; private final boolean active; private final List<Order> orders; public User(String name, boolean active, List<Order> orders) { this.name = name; this.active = active; this.orders = orders; } public boolean isActive() { return active; } public List<Order> getOrders() { return orders; } public String getName() { return name; } @Override public String toString() { return "User{name='" + name + "'}"; } } // Functional interface for handling exceptions in `Function` @FunctionalInterface interface ThrowingFunction<T, R> { R apply(T t) throws Exception; } // A helper class with wrapper methods for exception handling class ExceptionWrapper { // A wrapper for `Function` that catches exceptions public static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> function, R defaultValue) { return t -> { try { return function.apply(t); } catch (Exception e) { System.out.println(e.getMessage()); return defaultValue; } }; } }
Now, if an order contains a negative amount, the program doesn't stop but simply logs an error and replaces it with 0.0. This makes data processing more robust and practical for real-world use.
Thanks for your feedback!