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 theTry
monad.
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.Success
type) or — if something went wrong — an exception thrown by it (as an instance of theTry.Failure
type).
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 Vavr, fugue from Atlassian or FunctionalJava.
In the examples, you will see io.vavr.control.Try
The code
Below, I would like to focus on three aspects of theTry
container:
– 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:
On the other hand, when using theTry
container, the code evolves to this:
In the example above, you can see how simple it is to create an instance of theTry
— here, based on aCheckedRunnable
function provided as the argument.
Since calling CheckedRunnable.run()
returns void
, Try.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 Try
instance. 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 Try
for 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:
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.Failure
instance and can recover from it with some backup call.
In our example, the backup method is pretty simple and looks like the following:
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 Try
instance. For the latter, the result of a backup call is flattened, i.e. we work directly on it instead of a Try
wrapped 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.Failure
instance — returns an empty list.
Handle values
Let’s consider another situation where we call some functions returning values. Here, the Try
construction 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:
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 Try
instance returned by this method. Here is the body of the method:
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 theTry
instance 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 RawMeasurements
class.
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()
mapTry()
Try
API has its own xxxTry()
variant, where xxx
is the method dealing with unchecked exceptions only and xxxTry
handles 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:
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 Try
instances 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 User
entity 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 save
method 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 Try
construction 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