Every programmer has to deal with exceptions. There are various ways of handling them. Here is how you can control them in Java with some help of an external library.

Note: This blog was originally posted on Softwaremill’s blog


What and why

What is the purpose of exceptions in Java? Code that throws an exception signals that something went wrong. Let it be in our application or a hosting JVM.

Handling them is entirely doable. We just have to write a couple of try-catch statements here and there, and it works, right? Well, not really. At least for me.

I see two drawbacks here. The first one is code readability when handling exceptions in Java. The second issue is the difficulty to find a place of an exception’s origin, or is even harder to track how an exception arrived at a place of catching it.

However, we can handle exceptions in another way than we used to do. In the functional programming world, some smart people invented theTrymonad.


Give me some theory

So, what is aTry? It is the container wrapping a computation. It holds a value returned by the operation (as an instance of Try.Successtype) or — if something went wrong — an exception thrown by it (as an instance of theTry.Failuretype).

Moreover, it allows to chain computations, so you don’t have to worry about errors till returning a final value. Thus, we can handle the errors more elegantly, closer to the place of a “crime”. Such practice is especially useful when dealing with external libraries/tools we do not control.

As you can read in a good blogpost about Try in Scala: “It’s just like the Schrodinger’s cat story, you keep working with the box and delay opening it up until it’s completely necessary to know what’s happening in there.”


Use case

Scenario

Consider a service that finds information about air quality for a given city. In our example, we read a list of cities and their geo-location from a file. Next, for each one, we fetch air quality data using the service and, in the end, we store the data in a database.

Technological background

The standard Java library does not provide any Try implementation as Scala does. Where can you find one? In this case, you can choose from a few libraries like Vavrfugue from Atlassian or FunctionalJava.

In the examples, you will see theio.vavr.control.Tryconstruction from the Vavr library. Since version 1.0.0 of the library is (at the time of writing this) in alpha stage, I decided to use the latest 0.10.0 release.


The code

Below, I would like to focus on three aspects of theTrycontainer:
– handling side-effect calls,
– recovering from failure and
– handling values.

Catch exceptions from side-effects

Let’s start with something simple. Assume, our example database is a SQL one, and we connect to it through a JDBC driver. We have to start it before running the application and fetching all the data. We initiate the database by calling thestart()method, that may throw beloved SQLException.

With a standard try-catch, the case can look like the following:

try {
database.start();
  fetchMeasurements(geoCoordinatesReader, airlyService, measurementsRepository);
} catch (SQLException exc) {
  log.error("Cannot start the database", exc);
}

On the other hand, when using theTry container, the code evolves to this:

Try
.run(database::start)
.onSuccess(ignore -> log.info("Database started successfully"))
  .onFailure(exc -> log.error("Cannot start the database", exc))
  .andThen(
() -> fetchMeasurements(geoCoordinatesReader, airlyService, measurementsRepository)
);

In the example above, you can see how simple it is to create an instance of theTry — here, based on aCheckedRunnablefunction provided as the argument.

Since calling CheckedRunnable.run()returns voidTry.run()is a perfect shot for wrapping side-effects methods returning no value. It results with an instance of Try<Void>, holding no value in the case of success and an exception in a case of any error.

What’s next? First of all, we can log a result of the call by chaining onSuccess()and onFailure()methods. The former is used in case of a successful call, while the latter for calls ended with errors. Both methods trigger a consumer (provided as an argument) and return the non-changed Tryinstance. They are a perfect shot for calling side-effect actions on successful data and exceptions.

Next, we can chain other calls using the API of Try. As you can see in the example, we call the next method with theTry.andThen()construct — fetching of an air quality data in our example application.

Recover from failure

Till now, I have presented how to create Tryfor side-effect methods that return nothing. What about calling a method that returns something? And what can we do if such a call ends with an error? While the exact behaviour depends on your specific situation, you can see how to manage such a case in the following example:

Try.of(
() -> geoCoordinatesReader.fromCsvFile("./src/main/resources/cities.csv")
)
.onSuccess(coords -> log.info("Coordinates read: {}", coords))
  .onFailure(exc -> log.error("Cannot read coordinates from a file", exc))
  .recover(FileNotFoundException.class, (exc) -> provideBackupCoordinates())
  .getOrElse(List.empty());

First of all, we call a function that returns some data. In our example, this is a list of cities and their geo-locations. We wrap it using theTry.of()method. Next, with Try<List<GeoCoordinates>>in hand, we can log the result of the call, either a successful or an erroneous one as in the first code snippet.

What is new in this example is recovering from an exception thrown when reading a file. If the operation fails, we work on a Try.Failureinstance and can recover from it with some backup call.

In our example, the backup method is pretty simple and looks like the following:

private static List<GeoCoordinates> provideBackupCoordinates() {
 log.warn("Calling for backup data");
 return List.of(
GeoCoordinates.builder()
.city("Warszawa")
.latitude("52.25")
.longitude("21")
.build()
);
}

Vavr offers a bunch of recovery methods of two types: the ones returning expected data directly and the ones resulting with a data wrapped with another Tryinstance. For the latter, the result of a backup call is flattened, i.e. we work directly on it instead of a Trywrapped with another Try. This form of recovery is handy when we need to make some I/O operation to get backup data.

As a last resort, we can provide a default value when we extract the underlying value from Try. In the recovery example, you can see the code ends with Try.getOrElse(List.empty()). This one “unwraps” the container and returns data held by it or — if this is a Try.Failureinstance — returns an empty list.

Handle values

Let’s consider another situation where we call some functions returning values. Here, the Tryconstruction serves very well since we can manipulate values wrapped with the container.

When we have geo-coordinates in hand, it’s time to use them and call Airly to fetch air quality information. To do this, we have to assemble a URL we use to call the service. Next, make a call and, finally, read and parse a response.

We use three methods from Try’s API to complete this scenario: flatMap()map()and mapTry(). Here is the implementation:

Try.of(() -> Uri.create("http://airapi.airly.eu/v2/measurements/nearest/…"))
.flatMap(this::callAirly)
  .map(responseBody -> new RawMeasurements(coordinates.city(), responseBody))
  .mapTry(this::parse);

First of all, we begin with the creation of a URI. The operation can throw an exception. Thus it is a good idea to wrap it with Try. Next, we use the URI to call Airly using theTry.flatMap()method. What does it do? It applies callAirly(URI)method to the URI we have just created and returns with another Try which is flattened next. It means we work further on the Tryinstance returned by this method. Here is the body of the method:

private Try<String> callAirly(URI airlyUri) {
HttpRequest req = requestBuilder.uri(airlyUri).build();
return Try
.of(() -> httpClient.send(req, bodyHandler))
  .map(HttpResponse::body);
}

As you can see, the callAirly()method provides a stringified body of a response received from Airly. Since this is a raw JSON data, we need to parse it. For the convenience, I have transformed the string into an instance of a class holding the JSON data and the name of a city the data applies to. The transformation is done with the map()method — it applies a method to the value contained by theTryinstance and ends with a new one holding the result of the method or an unchecked exception thrown during computation. In the example, the method is a lambda returning a new instance of the RawMeasurementsclass.

As the last step, we parse the data. Instead of dealing with string, we extract various measurements. This part of the code can throw some checked exceptions, like JsonParsingException. This is a situation where the mapTry()method can help us. It works like a map() method but handles checked exceptions as well.

By the way, as you can see in sources of the Vavr, the map()method is just a shortcut of using mapTry(). It is a common pattern in the library: almost every method from TryAPI has its own xxxTry()variant, where xxxis the method dealing with unchecked exceptions only and xxxTryhandles checked ones as well.


Any risk ahead?

Are there any risks of using Try? Well, yes. We can find some.

For example, you have to be aware of using the onFailure()method. It is quite easy to call this method a couple of times for the same exception. Look at the code below:

Try.of(this::computation)
.onFailure(exc -> log.error("Computation failed!", exc))
  .andThen(this::storeInDb)
  .onFailure(exc -> log.error("Storing in database failed!", exc))
  .andThen(this::sendEmail)
  .onFailure(exc -> log.error("Email sending failed!", exc))
  .onSuccess(value -> log.info("Value computed, stored in db and sent in email"));

With the code above, if the computation()call fails, we log the exception thrown by the first line three times! This is because of chaining consecutive calls on the same instance of Try.Failure.

Another one is a possibility of ignoring Tryinstances returned from methods. This case should be important especially for developers providing API used by others. Consider the following situation.

We have an endpoint creating new users in our service. After successfully processing user data, we create Userentity and store it in a database using a method having a signature like this:Try<Void> save(User newUserEntity) { … }

When we call database.save(newUser)it is quite easy to forget to deal with an erroneous result. Additionally, if the savemethod does not log the results of the action, we have a piece of code that can swallow exceptions. Imagine a poor developer looking on production for possible reasons why no new user is created in a system, while there are no errors logged.

As usual, the introduction of new things in our toolbox opens a possibility they will be misused in some situations. We can mitigate or eliminate such risk by a constant broadening of our horizons, sharing our experiences and guiding the ones less experienced.


Wrap up

I hope this short reading convinces you to use the Tryconstruction in your projects. I like wrapping I/O and external libraries operations using this container and chaining consecutive method calls on it. Thanks to this, I can efficiently manage and track exceptions in my code. From my point of view, it allows writing more readable and elegant code comparing to the standard try-catch clauses in Java.

Do you have any remarks, thoughts or experiences and would like to share them? Leave a comment below or contact me!

Image by Joanna Malinowska from freeestocks.org


0 Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.