Functional Exceptions In Java

Functional Exceptions In Java

Brian Gerstle
Brian Gerstle

January 22, 2019

posts/2019-01-22-fnl-exceptions-in-java/functional-exceptions-java-social.jpg

Since the release of version 8, Java has been taking great strides to facilitate writing expressive and functional code. Unfortunately, these strides are often tripped up by one of Java's oldest and most pervasive features: exceptions. This post will suggest how to handle exceptions seamlessly in functional Java code, but first, we must examine the problems raised when an exception is encountered in one of Java's core functional APIs.

Errors In Functional Java APIs

So, what happens when an exception-throwing method is called in a lambda? Let's find out by writing a method that parses a URI from an environment variable. Since System.getenv can return null, we'll use Optional to convert the nullable return value into a URI:


/**
	* Get the API's base URL from the environment.
	* @return The URI, or null if the environment variable is missing.
	*/
URI apiBaseURLFromEnv() {
				Optional<URI> apiBaseURL = Optional.ofNullable(System.getenv("apiBaseURL")).map(s -> {
								try {
												return new URI(s);
								} catch (URISyntaxException e) {
												e.printStackTrace();
												return null;
								}
				});
				return apiBaseURL.orElse(null);
}

Ah, good old URISyntaxException. Normally, we'd add throws URISyntaxException to our method signature and move on, but that won't help us since Optional.map can't throw (or propagate) checked exceptions.[1] This leaves us with the following choices:

  1. Do the URI conversion outside of the lambda
  2. Return Optional.empty() from a catch block inside the lambda (substituting flatMap for map)
  3. Re-throw the URISyntaxException as a RuntimeException and catch it outside the lambda

You've probably gone with option 1 before, and while it gets the job done, it prevents us from getting the most out of Java's functional APIs.

Option 2 makes the return value ambiguous. Is it null because the environment variable is missing, or because it was an invalid URI? If you need to handle each of those cases differently, discarding the error isn't an option.

At this point, option 3 is looking like our best bet—until we look at the resulting code:


URI apiBaseURLFromEnv() throws URISyntaxException {
				Optional<URI> apiBaseURL;
				try {
								apiBaseURL = Optional
																.ofNullable(System.getenv("apiBaseURL"))
																.map(baseUrlString -> {
																				try {
																								return new URI(baseUrlString);
																				} catch (URISyntaxException e) {
																								// wrap as unchecked exception, catch outside of lambda
																								throw new RuntimeException("uri parsing error", e);
																				}
																});
				} catch (RuntimeException e) {
								// catch and re-throw the wrapped URISyntaxException
								if (e.getCause() instanceof URISyntaxException) {
												throw (URISyntaxException) e.getCause();
								} else {
												// something else weird happened, rethrow
												throw e;
								}
				}
				return apiBaseURL.orElse(null);
}

And at this point you might be looking back over your shoulder at option 1. Using functional APIs should be fluent, free of boilerplate, modular, and easy to read. And it can be! All it takes is a little inspiration and a couple hundred lines of Java code.[2] As it turns out, Java's Optional provides some insight into how we can clean up this code.

A Closer Look At Optional

Succinctly put, Optional was designed to provide compile- and run-time safety when handling null values and, in doing so, cut down on uncaught NullPointerException crashes. On top of being safer, Optional also has methods which make it easy to wrap, transform, and then unwrap nullable values:


Optional.ofNullable(stringOrNull)
				.map(String::toUpperCase)
				.orElse("default");

It's worth explaining these steps in detail, since they're key to understanding functional programming in Java:

Step one takes a possibly null value and wraps it in an Optional, which will either be present (value is not null) or empty (value is null). Depending on which state it's in, the Optional will respond to method calls differently, keeping us safe while also saving us from manual null checks.

Step two might seem foreign, but I'm willing to bet that you've written something similar to it. Does this ring any bells?


String foo = /* ??? */;
if (foo == null) {
				return null;
}
return foo.toUpperCase();

If so, then you've already used Optional.map! Generally speaking, map is a way to transform something inside an Optional without having to unwrap it, or check if it's null. Note that the mapping function never gets called if the Optional is empty.

Finally, the third step unwraps the Optional using its orElse method. This can be read as: "return the value inside the Optional if it has one, 'or else' return 'default'." There are a variety of ways to unwrap an Optional, and they're worth studying if you plan to get the most out of its API. For now, focus on orElse and try to imagine what its result will be for different inputs (e.g. null, "foo", and ""). Understanding the ins and outs of Optional is a crucial step in learning functional programming.

With Optional, Java programmers can deal with null in a way that fits perfectly within Java's other functional APIs, like Stream. However, we're still left to handle exceptions imperatively with try/catch which, as we've seen, doesn't work so well with functional APIs. Ideally, we could handle exceptions functionally the same way Optional lets us deal with null. This would allow us to use APIs that throw checked exceptions inside lambdas without using RuntimeException or abandoning functional APIs altogether. Before we get into designing a functional, error-handling API in Java, let's see what we can learn from how another functional language has decided to address this problem.

Error Handling In Haskell

When it comes to learning functional programming, Haskell has a lot of great examples to study—and error handling is no exception. In Haskell, errors can be handled in one of two ways:

  1. "Catch" errors in functions that will be invoked if an exception is thrown (e.g. using catch or its derivatives like bracketOnError from the Control.Exception module)
  2. Return the error as part of the result, using a type like Either that you can pass around, transform, and unwrap as needed

Option 2 sounds like exactly what we're looking for! Either is a type in Haskell that can "either" be a Left or a Right value. Similar to Java's Optional, Either has functions that behave differently if its value is an error (Left) or a successful result (Right). Let's see how it works:


main = do
				-- 1
				putStr "Enter file to read: "
				path <- getLine
				-- 2
				result <- try $ readFile path :: IO (Either SomeException String)
				-- 3
				let loudResult = fmap uppercaseString result
				-- 4
				putStrLn $ case loudResult of
								Left e -> "Failed to read file: " ++ show e
								Right r -> r

In the example above, we:

  1. Ask the user which file to open
  2. try to read the file, which returns a result of type Either SomeException String, which is either an error that occurred (Left :: SomeException) or the contents of the file (Right :: String)
  3. Transform the wrapped value inside result (upper-casing the file contents)
  4. Unwrap the Either and if it's an error (Left), print an error message, otherwise return the Right value (transformed from the prior step)

If you run this program and tell it to read a file containing "Hello world!", it will output:

HELLO WORLD!

But, if you ask it to read a file that doesn't exist, it will output:

Failed to read file: doesntexist: openFile: does not exist (No such file or directory)

Just like in the Optional example, we wrapped a value, transformed it, then unwrapped it at the end. Once again, the key is in being able to do transformations on and pass around result, waiting to handle any raised exceptions until the end. Let's see if we can bring this functionality to Java.

Functional Error Handling In Java

Based on what we've seen so far, we'll need a generic wrapper class similar to Optional, but in order to emulate Haskell's Either, it needs to contain either an error or a value:

public class Result<V, E extends Throwable> {
				private final V value;
				private final E error;

				private Result(V value, E error) {
								this.value = value;
								this.error = error;
				}

				public static <V, E extends Throwable> Result<V, E> failure(E error) {
								return new Result<>(null, Objects.requireNonNull(error));
				}

				public static <V, E extends Throwable> Result<V, E> success(V value) {
								return new Result<>(Objects.requireNonNull(value), null);
				}
}

Here we have a class Result which can either be initialized with a value of type V via Result.success, or an error of type E via Result.failure. Wrapping values and errors is a good first step, but the goal is to avoid try/catch, so let's add a wrapper for that too:


public static <V, E extends Throwable> Result<V, E> attempt(CheckedSupplier<? extends V, ? extends E> p) {
				try {
								return Result.success(p.get());
				} catch (Throwable e) {
								@SuppressWarnings("unchecked")
								E err = (E)e;
								return Result.failure(err);
				}
}

Now Result can be initialized with the "result" of a lambda that throws a checked exception. You might be thinking, "but I thought lambdas couldn't throw exceptions!" Function lambdas can't, but we're using our own @FunctionalInterface called CheckedSupplier, and it looks like this:


@FunctionalInterface
public interface CheckedSupplier<V, E extends Throwable> {
				V get() throws E;
}

In other words, a function that returns a value of type V and throws an exception of type E. Adding the @FunctionalInterface annotation to our interface allows us to write lambdas as shorthand for instances of that interface. This means that when Result.attempt is called, it invokes the get method of our CheckedSupplier, and returns either a Result.success wrapping the value, or a Result.failure wrapping the exception. There's a bit more going on here with wildcard type parameters (?) and unchecked casting, but let's put that aside for now and implement the functional API for transformations, map:


public <T> Result<T, E> map(Function<? super V, ? extends T> mapper) {
				return Optional.ofNullable(error)
								.map(e -> Result.<T, E>failure(e))
								.orElseGet(() -> Result.success(mapper.apply(value)));
}

Once again, borrowing from both Java's Optional and Haskell's Either, we want map to return a new wrapper for the transformed value. Unless it's an error, in which case it should return a wrapped error. Lastly, we need the ability to unwrap the Result and decide how to handle each of its possible states. For now, let's keep it simple by implementing a function that either returns the value, or throws the error:


public V orElseThrow() throws E {
				return Optional.ofNullable(value).orElseThrow(() -> error);
}

With this, we can defer handling any exceptions until after all the transformations have been applied. This laziness is the key to programming in a functional, even declarative style. Let's put it all together and revisit our very first example of parsing a URI:


/**
	* Get the API's base URL from the environment.
	* @return The URI, or null if the environment variable is missing.
	*/
URI apiBaseURLFromEnv() throws URISyntaxException {
				Optional<Result<URI, URISyntaxException>> apiBaseURL =
								Optional.ofNullable(System.getenv("apiBaseURL"))
																.map(urlString -> {
																				return Result.attempt(() -> new URI(urlString));
																});
				if (!apiBaseURL.isPresent()) {
								return null;
				}
				return apiBaseURL.get().orElseThrow();
}

Now we can call new URI() from within Optional.map, and defer throwing the URISyntaxException until we try to unwrap the result! But, even though we've solved the problems set out at the beginning of this post, the code still has an isPresent check and an intermediate variable assignment. Good thing we're only just getting started! With some more functional techniques and new methods on our Result type, we can make the method completely functional:


URI apiBaseURLFromEnv() throws URISyntaxException {
				return Result.<String, Exception>attempt(Optional.ofNullable(System.getenv("apiBaseURL"))::get)
								.flatMap(Result.from(URI::new))
								.map(Optional::of)
								.recover(NoSuchElementException.class, Optional::empty)
								.orElseThrowAs(e -> (URISyntaxException)e)
								.orElse(null);
}

There's a fair bit going on here with method references, type casting, and other functional concepts we haven't talked about—mainly flatMap[3] and Optional.get. I'll let you look up the details, but at a high level, this method:

  1. Wraps either the result of getting the apiBaseURL environment variable, either a String or NoSuchElementException
  2. Tries to parse it, returning a result of either URI or URISyntaxException
  3. Wrap that URI in an Optional<URI>, which is either a non-empty Optional<URI> or one of the above exceptions
  4. In case step 1 resulted in a NoSuchElementException, fall back to an empty Optional<URI>
  5. Unwrap the Result, throwing the URISyntaxException if it was encountered in step 2
  6. Unwrap the Optional<URI>, returning either the URI from step 3, or null from step 4

Now that we can call exception-throwing methods in lambdas with Result.attempt, we can also use them in Stream transformations:


List<Integer> parseInts(List<String> intStrings) throws NumberFormatException {
				return intStrings
												.stream()
												.map(i -> Result.<Integer, NumberFormatException>attempt(() -> parseInt(i)))
												.collect(new ResultCollector<>())
												.orElseThrow();
}

If intStrings contains "1", "2", "3", this method will return 1, 2, 3. But if it contains "1", "2", "can't parse this", the method will throw a NumberFormatException.

This post covered some crucial elements of functional programming—in two languages! I hope it helped you wrap your head around Optional, map, and Java's approach to functional programming in general (wrap, transform, unwrap). If you're interested in learning more about how the Result Java class works, or you want to use it in your project, there's an implementation on GitHub. Thanks for reading, and happy (functional) coding!

[1] Remember, Java's lambdas are essentially syntactic sugar for a class that implements a @FunctionalInterface. Optional.map accepts an object that implements the Function interface's apply method, which can be written as a lambda expression. Note the lack of throws in the type signature, which prohibits the implementation (i.e. lambda body) from throwing checked exceptions.

[2] 260 lines of source code in the result-java result package, as measured by the Statistic plugin for IntelliJ at time of writing.

[3] flatMap is like map, but can return an instance of whatever the "wrapper" (or Monad) is being flat-mapped. In Java, Optional.flatMap takes a function that returns an Optional, Stream.flatMap takes a function that returns a Stream.