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:
- Do the URI conversion outside of the lambda
-
Return
Optional.empty()
from acatch
block inside the lambda (substitutingflatMap
formap
) -
Re-throw the
URISyntaxException
as aRuntimeException
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:
-
"Catch" errors in functions that will be invoked if an exception is thrown (e.g. using
catch
or its derivatives likebracketOnError
from theControl.Exception
module) -
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:
- Ask the user which file to open
-
try
to read the file, which returns aresult
of typeEither SomeException String
, which is either an error that occurred (Left :: SomeException
) or the contents of the file (Right :: String
) -
Transform the wrapped value inside
result
(upper-casing the file contents) -
Unwrap the
Either
and if it's an error (Left
), print an error message, otherwise return theRight
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:
-
Wraps either the result of getting the
apiBaseURL
environment variable, either aString
orNoSuchElementException
-
Tries to parse it, returning a result of either
URI
orURISyntaxException
-
Wrap that
URI
in anOptional<URI>
, which is either a non-emptyOptional<URI>
or one of the above exceptions -
In case step 1 resulted in a
NoSuchElementException
, fall back to an emptyOptional<URI>
-
Unwrap the
Result
, throwing theURISyntaxException
if it was encountered in step 2 -
Unwrap the
Optional<URI>
, returning either theURI
from step 3, ornull
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
.