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 inside map(), 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

Suppose readRate() 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);
  }
}
 OUTPUT on compiling the above code
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
The lambda 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.
Why checked exceptions do not fit Java 8 lambdas, and four ways to handle them - catch, wrap, turn into values, or use a loop

Java 8 code example of the four approaches

The example uses a Rates 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));
  }
}
 OUTPUT of the above code
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-catch inside 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: ThrowingFunction is a home-made functional interface whose apply() is allowed to throw. Rates::lookup matches it, and unchecked() turns it into a normal Function that rethrows any checked exception wrapped in a RuntimeException. Without IT, the stream runs normally; with IT, it stops at the first failure, and the original exception is available through getCause() where the stream is called.
  • unchecked() rethrows a RuntimeException as it is, rather than wrapping it a second time.
  • Part 3: optional() turns each failure into Optional.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 in UncheckedIOException, 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 for loop 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 inside map(): 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 - Handling Checked Exceptions in Lambdas - JavaBrahman