Java 8 - Handling Checked Exceptions in Lambdas
Introduction
This tutorial explains how to handle checked exceptions in Java 8 lambdas and streams - one of the most common frustrations when moving from loops to streams. We will start with the compile error you get when a method that throws a checked exception is called insidemap(), and why the functional interfaces cause it. We will then compare four ways out: handling the exception inside the lambda, wrapping it in an unchecked exception, turning failures into values, and going back to a loop. Lastly, we will run an example of each, look at Java 8's UncheckedIOException, and list which approach suits which situation.(For the difference between checked and unchecked exceptions, see Checked vs Unchecked Exceptions in JavaClick to read: Checked vs Unchecked Exceptions in Java.)
The problem
SupposereadRate() reads a department's bonus rate from a file, and so declares throws IOException. Calling it inside a stream does not compile -
Java 8 code calling a method that throws a checked exception inside map()
//LambdaCompileError.java
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LambdaCompileError {
// Reads a rate from a file in a real application, so it may throw IOException
static double readRate(String department) throws IOException {
return 0.10;
}
public static void main(String[] args) {
List<String> departments = Arrays.asList("Finance", "Sales");
List<Double> rates = departments.stream()
.map(d -> readRate(d)) // a checked exception inside map()
.collect(Collectors.toList());
System.out.println(rates);
}
}
LambdaCompileError.java:17: error: unreported exception IOException; must be caught or declared to be thrown
.map(d -> readRate(d)) // a checked exception inside map()
^
1 error
d -> readRate(d) is an implementation of Function.apply(), and apply() is declared without a throws clause. A method may only throw the checked exceptions its declaration allows, so the lambda's body may not throw IOException at all. Adding throws IOException to main() does not help: the exception would be thrown inside apply(), not inside main(). None of the java.util.functionClick to read: java.util.function interfaces declares checked exceptions, so the same applies to filter(), forEach() and every other stream operation.
Java 8 code example of the four approaches
The example uses aRates class whose lookup() method throws a checked RateNotFoundException for an unknown department. There are rates for Finance and Sales, but not for IT.
Rates.java - a method that throws a checked exception
//Rates.java
import java.util.HashMap;
import java.util.Map;
public class Rates {
// A checked exception: callers must catch it or declare it
@SuppressWarnings("serial")
public static class RateNotFoundException extends Exception {
public RateNotFoundException(String department) {
super("No bonus rate for " + department);
}
}
private static final Map<String, Double> BONUS_RATES = new HashMap<>();
static {
BONUS_RATES.put("Finance", 0.10);
BONUS_RATES.put("Sales", 0.15);
}
public static double lookup(String department) throws RateNotFoundException {
Double rate = BONUS_RATES.get(department);
if (rate == null) {
throw new RateNotFoundException(department);
}
return rate;
}
}
Java 8 code handling checked exceptions in streams with try-catch, a wrapper and Optional
//CheckedInLambdas.java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
public class CheckedInLambdas {
// A Function whose apply() is allowed to throw checked exceptions
@FunctionalInterface
interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
// Adapts a ThrowingFunction to a Function by wrapping checked exceptions
static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
// Adapts a ThrowingFunction to a Function that returns Optional.empty() on failure
static <T, R> Function<T, Optional<R>> optional(ThrowingFunction<T, R> f) {
return t -> {
try {
return Optional.ofNullable(f.apply(t));
} catch (Exception e) {
return Optional.empty();
}
};
}
public static void main(String[] args) {
List<String> departments = Arrays.asList("Finance", "IT", "Sales");
// 1. Handle the exception inside the lambda
List<Double> withDefault = departments.stream()
.map(d -> {
try {
return Rates.lookup(d);
} catch (Rates.RateNotFoundException e) {
return 0.0;
}
})
.collect(Collectors.toList());
System.out.println("1. try-catch in the lambda: " + withDefault);
// 2. Wrap it in an unchecked exception and handle it outside the stream
System.out.println("2. without IT: " + Arrays.asList("Finance", "Sales").stream()
.map(unchecked(Rates::lookup))
.collect(Collectors.toList()));
try {
departments.stream().map(unchecked(Rates::lookup)).collect(Collectors.toList());
} catch (RuntimeException e) {
Throwable cause = e.getCause();
System.out.println(" with IT: stream stopped by "
+ cause.getClass().getSimpleName() + ": " + cause.getMessage());
}
// 3. Turn failures into Optional.empty() and skip them
List<Double> found = departments.stream()
.map(optional(Rates::lookup))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
System.out.println("3. skipping failures: " + found);
// 4. Keep the failures too, to report them
Map<Boolean, List<String>> split = departments.stream()
.collect(Collectors.partitioningBy(d -> optional(Rates::lookup).apply(d).isPresent()));
System.out.println("4. with a rate: " + split.get(true)
+ ", without: " + split.get(false));
}
}
1. try-catch in the lambda: [0.1, 0.0, 0.15] 2. without IT: [0.1, 0.15] with IT: stream stopped by RateNotFoundException: No bonus rate for IT 3. skipping failures: [0.1, 0.15] 4. with a rate: [Finance, Sales], without: [IT]
Explanation of the code
- Part 1: a
try-catchinside the lambda handles the exception where it happens and returns 0.0 for IT. The stream carries on. It works, but every such lambda grows to six lines, and a default value can hide a real problem. - Part 2:
ThrowingFunctionis a home-made functional interface whoseapply()is allowed to throw.Rates::lookupmatches it, andunchecked()turns it into a normalFunctionthat rethrows any checked exception wrapped in aRuntimeException. Without IT, the stream runs normally; with IT, it stops at the first failure, and the original exception is available throughgetCause()where the stream is called. unchecked()rethrows aRuntimeExceptionas it is, rather than wrapping it a second time.- Part 3:
optional()turns each failure intoOptional.empty(), so every department is processed and the failures are filtered out. See OptionalClick to read: Optional. - Part 4:
partitioningBy()splits the departments into those that have a rate and those that do not, so the failures can be reported instead of silently dropped. See the partitioningBy() tutorialClick to read: partitioningBy() tutorial.
UncheckedIOException
For the most common case, I/O, Java 8 added a standard wrapper:java.io.UncheckedIOException, an unchecked exception that wraps an IOException. The JDK itself uses it - Files.lines() and BufferedReader.lines() throw it when reading fails part-way through a stream. Use it in your own wrappers too, so that callers can catch I/O failures specifically -
Wrapping an IOException in UncheckedIOException
List<String> contents = paths.stream()
.map(path -> {
try {
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})
.collect(Collectors.toList());
Which approach to choose
- The failure is expected and has a sensible fallback (a missing rate means no bonus): handle it inside the lambda, ideally by moving the try-catch into a small named method so the stream stays readable.
- Any failure means the whole operation has failed (one unreadable file spoils the report): wrap it with a helper such as
unchecked(), or inUncheckedIOException, and catch it where the stream is called. Always keep the original as the cause. - You want to process everything and report the failures: turn each failure into a value -
Optional, or a small result class holding either the value or the exception - and split the results afterwards. - The code is mostly about I/O and error handling: a plain
forloop is often clearer than a stream full of try-catch blocks, and checked exceptions work there as usual. - Avoid "sneaky throws": a generics trick can make the compiler let a checked exception escape a lambda undeclared. It works, but callers cannot catch the exception by type without warnings, and readers are surprised - see the Principle of Least AstonishmentClick to read: Principle of Least Astonishment.
Summary
In this tutorial we first saw why a method that throws a checked exception cannot be called directly insidemap(): the functional interfaces declare no checked exceptions. Next we compared four ways out. We then ran an example handling the exception inside the lambda, wrapping it with a reusable unchecked() helper built on a ThrowingFunction interface, turning failures into Optional.empty(), and partitioning successes from failures. We then looked at Java 8's UncheckedIOException. Lastly, we listed which approach suits which situation.Java 8 Tutorials
Java 8 Lambdas: Effectively Final, Capture and thisClick to read tutorial: Java 8 Lambdas: Effectively Final, Capture and this Java 8 ConcurrentHashMap, LongAdder and StampedLockClick to read tutorial: Java 8 ConcurrentHashMap, LongAdder and StampedLock Java 8 CompletableFuture Tutorial with ExamplesClick to read tutorial: Java 8 CompletableFuture Tutorial with Examples Java 8 CompletableFuture: Combining and Error HandlingClick to read tutorial: Java 8 CompletableFuture: Combining and Error Handling Java 8 Parallel Streams Tutorial with ExamplesClick to read tutorial: Java 8 Parallel Streams Tutorial with Examples Java 8 Collectors.toMap Tutorial with ExamplesClick to read tutorial: Java 8 Collectors.toMap Tutorial with Examples Java 8 Custom Collector with Collector.of() TutorialClick to read tutorial: Java 8 Custom Collector with Collector.of() Tutorial
Java 8 Lambdas: Effectively Final, Capture and thisClick to read tutorial: Java 8 Lambdas: Effectively Final, Capture and this Java 8 ConcurrentHashMap, LongAdder and StampedLockClick to read tutorial: Java 8 ConcurrentHashMap, LongAdder and StampedLock Java 8 CompletableFuture Tutorial with ExamplesClick to read tutorial: Java 8 CompletableFuture Tutorial with Examples Java 8 CompletableFuture: Combining and Error HandlingClick to read tutorial: Java 8 CompletableFuture: Combining and Error Handling Java 8 Parallel Streams Tutorial with ExamplesClick to read tutorial: Java 8 Parallel Streams Tutorial with Examples Java 8 Collectors.toMap Tutorial with ExamplesClick to read tutorial: Java 8 Collectors.toMap Tutorial with Examples Java 8 Custom Collector with Collector.of() TutorialClick to read tutorial: Java 8 Custom Collector with Collector.of() Tutorial