Handling Exceptions in Java With Try-Catch Block and Vavr Try
Today, we are going to talk about a very important topic — exception handling in Java. While this subject may seem overly discussed at times, not every article out there contains useful and relevant information.
The most common exception handling mechanism in Java is often associated with the try-catch
block. We use this to catch an exception and then provide logic that would then be executed in the case where an exception occurs.
You may also like: Using Java Optional Vs. Vavr Option
It's also true that you don’t need to put all exceptions inside these blocks. On another hand, if you are working on the software design of your application, a built-in exception handling mechanism may not be what you want. In such situations, you can try to use an alternative – the Vavr Try construction. Last time, we had a discussion on using Vavr in Java apps in regards to the null value check.
In this post, we are going to explore different approaches to Java exception handling and discuss how to use Vavr Try as a substitute for built-in methods. Let's get started!
Handling Exceptions in Java
As introduction let review how Java permits us to handle exceptions. If you don’t remember it, the exception in Java states for unwanted or unexpected event, which occurs during the execution of a program i.e at run time, that disrupts the normal flow of the program’s instructions. Java offers us the aforesaid try-catch
mechanism to catch exceptions. Let briefly check how does it work.
What Happens if You Don’t Handle Exceptions?
Ok, let take a look at a very common example. Here is a code snippet where we have a JDBC code. Take a look:
Connection connection = dataSource.getConnection();
String updateNameSql = "UPDATE employees SET name=? WHERE emp_id=?";
PreparedStatement preparedStatement = connection.prepareStatement(updateNameSql);
Frankly speaking, your IDE does not permit you even to write this code and requires you to surround it with try-catch block. Like this:
try {
Connection connection = dataSource.getConnection();
String updateNameSql = "UPDATE employees SET name=? WHERE emp_id=?";
PreparedStatement preparedStatement = connection.prepareStatement(updateNameSql);
} catch (SQLException ex){
}
Please note: We could refactor it to try-with-resources, but we will talk about that a bit later. For now, this code will do.
So, why do we have to write this code like this? Because SQLException
is a checked exception.
You have to declare these exceptions in either a method or a constructor’s throws clause if they can be thrown by the execution of the method or the constructor and propagate outside of the method or constructor boundary. Methods that we use in our example throw a SQLException
if a database access error occurs. Therefore, we surround it with a try-catch
block.
Java validates these exceptions during compilation — and that's what makes them different from run-time exceptions (we will talk about these a bit later).
But You Don’t Have to Handle All Exceptions...
However, not every exception should be surrounded by a try-catch
block.
Case 1. Runtime Exceptions
Java exceptions are subclasses of Throwable
, but some of them are subclassed from the RuntimeException
class. Take a look at the graph below that presents a hierarchy of Java exceptions:
Please note that runtime exceptions are a specific group. According to Java specification, a recovery from these exceptions may still be possible. As an example, let's recall ArrayIndexOutOfBoundsException
. Have a look at the sample code snippet below:
int numbers[] = [1,43,51,0,9];
System.out.println(numbers[6]);
Here, we have an array of integers with 5 values (0-4 position). When we try to retrieve a value that is definitely out of range (index= 6), Java will throw an ArrayIndexOutOfBoundsException
.
This indicates that the index we try to call is either negative, greater than, or equal to the size of the array. As I said, these exceptions can be recovered, so they are not checked during compilation. That means you can still write code such as:
int numbers[] = [1,43,51,0,9];
int index = 6;
try{
System.out.println(numbers[index]);
} catch (ArrayIndexOutOfBoundsException ex){
System.out.println("Incorrect index!");
}
…but you are not required to do so.
Case 2. Errors
Error
is another tricky concept. Take another look at the graph above – errors are there, but they are usually not handled. Why? Often, this is due to the fact that there is nothing that a Java program can do to recover from the error, e.g. errors indicate serious problems that a reasonable application should not even try to catch.
Let's think about the memory error – java.lang.VirtualMachineError
. This error indicates that the JVM is broken or has run out of resources necessary for it to continue operating. Or, in other words, if an app runs out of memory, it simply can’t allocate additional memory resources.
Certainly, if a failure is a result of holding a lot of memory that should be made free, an exception handler could attempt to free it (not directly itself but it can call the JVM to do it). And while such a handler may be useful in this context, such an attempt may not be successful.
Variants of the Try-Catch Block
The above-mentioned way of writing a try-catch
statement is not the only one that is available in Java. There are other methods: try-with-resources, try-catch-finally, and multiple catch blocks. Let make a quick trip to these different groups.
Variant 1. Try-With-Resources
The Try-with-resources block was introduced in Java 7 and allows developers to declare resources that must be closed after the program is finished with it. We can include a resource in any class that implements the AutoCloseable
interface (that is a specific marker interface). We can refactor the mentioned JDBC code like this:
try (Connection connection = dataSource.getConnection){
String updateNameSql = "UPDATE employees SET name=? WHERE emp_id=?";
PreparedStatement preparedStatement = connection.prepareStatement(updateNameSql);
} catch (SQLException ex){
//..
}
Java ensures us that Connection
will be closed after the code is executed. Before this construction, we had to close resources inside the finally
block explicitly.
Variant 2. Try + Finally
The finally
block is executed in any case, e.g. in the case of success or in the case of an exception. Inside it, you need to put code that would be executed after:
FileReader reader = null;
try {
reader = new FileReader("/text.txt");
int i=0;
while(i != -1){
i = reader.read();
System.out.println((char) i );
}
} catch(IOException ex1){
//...
} finally{
if(reader != null){
try {
reader.close();
} catch (IOException ex2) {
//...
}
}
}
Please note that there is a drawback to this approach: If an exception is thrown inside the finally
block, it makes it interrupted. Therefore, we have to handle the exception as normal. Use try-with-resources with closeable resources and avoid the closing of resources inside the finally
block.
Variant 3. Multiple Catches
In the end, Java permits us to catch exceptions multiple times with one try-catch block. This is useful when methods throw several types of exceptions and you would like to differentiate a logic for each case. As an example, let take this fictitious class with methods that throw several exceptions:
class Repository{
void insert(Car car) throws DatabaseAccessException, InvalidInputException {
//...
}
}
//...
try {
repository.insert(car);
} catch (DatabaseAccessException dae){
System.out.println("Database is down!");
} catch (InvalidInputException iie){
System.out.println("Invalid format of car!");
}
What do you need to remember here? Generally, we assume, in this code, those exceptions are at the same level. But you have to order catch blocks from the most specific to the most general. For example, say the catch for ArithmeticException
must come before the catch for Exception
. Something like that.
In this section, we reviewed how to handle exceptions in Java using a built-in approach. Now, let's have a look at how to do it with the Vavr library.
Vavr Try
We reviewed a standard way of catching Java exceptions. Another approach is to use the Vavr Try
class. First, add the Vavr library dependency:
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.2</version>
</dependency>
Try Container
Vavr includes Try
class that is a monadic container type, which represents a computation that may either result in an exception, or return a successfully computed value. This result can be in the form of Success
or Failure
. Take a look at this code:
class CarsRepository{
Car insert(Car car) throws DatabaseAccessException {
//...
}
Car find (String id) throws DatabaseAccessException {
//..
}
void update (Car car) throws DatabaseAccessException {
//..
}
void remove (String id) throws DatabaseAccessException {
//..
}
}
In calling this code, we will use the try-catch
blocks to handle DatabaseAccessException
. But another solution is to refactor it with Vavr. Check out this code snippet:
class CarsVavrRepository{
Try<Car> insert(Car car) {
System.out.println("Insert a car...");
return Try.success(car);
}
Try<Car> find (String id) {
System.out.println("Finding a car...");
System.out.println("..something wrong with database!");
return Try.failure(new DatabaseAccessException());
}
Try<Car> update (Car car) {
System.out.println("Updating a car...");
return Try.success(car);
}
Try<Void> remove (String id) {
System.out.println("Removing a car...");
System.out.println("..something wrong with database!");
return Try.failure(new DatabaseAccessException());
}
}
Now, we can handle database problems with Vavr.
Handling Success
When we receive a successfully computed result, we receive a Success
:
@Test
void successTest(){
CarsVavrRepository repository = new CarsVavrRepository();
Car skoda = new Car("skoda", "9T4 4242", "black");
Car result = repository.insert(skoda).getOrElse(new Car("volkswagen", "3E2 1222", "red"));
Assertions.assertEquals(skoda.getColor(), result.getColor());
Assertions.assertEquals(skoda.getId(), result.getId());
}
Note that Vavr.Try
, as Vavr.Option
, offers us a handy getOrElse
method where we put a default value in case of failure. We can use this logic with a “problematic” method, for example with find
.
Handling Failure
In another case, we would handle a Failure
:
@Test
void failureTest(){
CarsVavrRepository repository = new CarsVavrRepository();
// desired car
Car bmw = new Car("bmw", "4A1 2019", "white");
// failure car
Car failureCar = new Car("seat", "1A1 3112", "yellow");
Car result = repository.find("4A1 2019").getOrElse(failureCar);
Assertions.assertEquals(bmw.getColor(), result.getColor());
Assertions.assertEquals(bmw.getId(), result.getId());
}
Run this code. This test will fail because of assertions the error:
org.opentest4j.AssertionFailedError:
Expected :white
Actual :yellow
That means that because we hardcoded a failure in the find
method, we receive a default value. Instead of returning a default value, we can do other things with a result in case of error. You can chain functions with Option
that makes your code much more functional:
repository.insert(bmw).andThen(car -> {
System.out.println("Car is found "+car.getId());
}).andFinally(()->{
System.out.println("Finishing");
});
Alternatively, you can execute code with the received exception, like this:
repository.find("1A9 4312").orElseRun(error->{
//...
});
Generally speaking, Vavr Try
is a feature-rich solution that you can use in order to transform your codebase in a more functional way. No doubt it's real power is unleashed in combination with other Vavr classes, like Option or Collections. But anyhow, even without them, Vavr Try
is a real alternative for Java try-catch
blocks if you want to write more functional-style code.
Conclusion
An exception handling mechanism in Java is commonly associated with the try-catch
block that we use in order to catch an exception and to provide logic that would be executed when an exception occurs. Also, it is true that we don’t need to put all exceptions inside these blocks. In this post, we explored how to do so using the Vavr library.
I hope you enjoyed!
Further Reading
Using Java Optional Vs. Vavr Option
9 Best Practices to Handle Exceptions in Java