1. Introduction
It’s pretty common in Java to work with nested exceptions as they can help us track the source of an error.
When we deal with these kinds of exceptions, sometimes we may want to know the original problem that caused the exception so our application can respond differently for each case. This is especially useful when we work with frameworks that wrap the root exceptions into their own.
In this short article, we’ll show how to get the root cause exception using plain Java as well as external libraries such as Apache Commons Lang and Google Guava.
2. An Age Calculator App
Our application will be an age calculator that tells us how old a person is from a given date received as String in ISO format. We’ll handle 2 possible error cases when parsing the date: a poorly-formatted date and a date in the future.
Let’s first create exceptions for our error cases:
static class InvalidFormatException extends DateParseException { InvalidFormatException(String input, Throwable thr) { super("Invalid date format: " + input, thr); } } static class DateOutOfRangeException extends DateParseException { DateOutOfRangeException(String date) { super("Date out of range: " + date); } }
Both exceptions inherit from a common parent exception that will make our code a bit clearer:
static class DateParseException extends RuntimeException { DateParseException(String input) { super(input); } DateParseException(String input, Throwable thr) { super(input, thr); } }
After that, we can implement the AgeCalculator class with a method to parse the date:
static class AgeCalculator { private static LocalDate parseDate(String birthDateAsString) { LocalDate birthDate; try { birthDate = LocalDate.parse(birthDateAsString); } catch (DateTimeParseException ex) { throw new InvalidFormatException(birthDateAsString, ex); } if (birthDate.isAfter(LocalDate.now())) { throw new DateOutOfRangeException(birthDateAsString); } return birthDate; } }
As we can see, when the format is wrong we wrap the DateTimeParseException into our custom InvalidFormatException.
Finally, let’s add a public method to our class that receives the date, parses it and then calculates the age:
public static int calculateAge(String birthDate) { if (birthDate == null || birthDate.isEmpty()) { throw new IllegalArgumentException(); } try { return Period .between(parseDate(birthDate), LocalDate.now()) .getYears(); } catch (DateParseException ex) { throw new CalculationException(ex); } }
As shown, we’re wrapping the exceptions again. In this case, we wrap them into a CalculationException that we have to create:
static class CalculationException extends RuntimeException { CalculationException(DateParseException ex) { super(ex); } }
Now, we’re ready to use our calculator by passing it any date in ISO format:
AgeCalculator.calculateAge("2019-10-01");
And if the calculation fails, it would be useful to know what the problem was, wouldn’t it? Keep reading to find out how we can do that.
3. Find the Root Cause Using Plain Java
The first way we’ll use to find the root cause exception is by creating a custom method that loops through all the causes until it reaches the root:
public static Throwable findCauseUsingPlainJava(Throwable throwable) { Objects.requireNonNull(throwable); Throwable rootCause = throwable; while (rootCause.getCause() != null && rootCause.getCause() != rootCause) { rootCause = rootCause.getCause(); } return rootCause; }
Notice that we’ve added an extra condition in our loop to avoid infinite loops when handling recursive causes.
If we pass an invalid format to our AgeCalculator, we’ll get the DateTimeParseException as the root cause:
try { AgeCalculator.calculateAge("010102"); } catch (CalculationException ex) { assertTrue(findCauseUsingPlainJava(ex) instanceof DateTimeParseException); }
However, if we use a future date we’ll get a DateOutOfRangeException:
try { AgeCalculator.calculateAge("2020-04-04"); } catch (CalculationException ex) { assertTrue(findCauseUsingPlainJava(ex) instanceof DateOutOfRangeException); }
Furthermore, our method also works for non-nested exceptions:
try { AgeCalculator.calculateAge(null); } catch (Exception ex) { assertTrue(findCauseUsingPlainJava(ex) instanceof IllegalArgumentException); }
In this case, we get an IllegalArgumentException since we passed in null.
4. Find the Root Cause Using Apache Commons Lang
We’ll now demonstrate finding the root cause using third-party libraries instead of writing our custom implementation.
Apache Commons Lang provides an ExceptionUtils class which provides some utility methods to work with exceptions.
We’ll use the getRootCause() method with our previous example:
try { AgeCalculator.calculateAge("010102"); } catch (CalculationException ex) { assertTrue(ExceptionUtils.getRootCause(ex) instanceof DateTimeParseException); }
We get the same root cause as before. The same behavior applies to the other examples that we’ve listed above.
5. Find the Root Cause Using Guava
The last way we’re going to try is by using Guava. Similar to Apache Commons Lang, it provides a Throwables class with a getRootCause() utility method.
Let’s try it out with the same example:
try { AgeCalculator.calculateAge("010102"); } catch (CalculationException ex) { assertTrue(Throwables.getRootCause(ex) instanceof DateTimeParseException); }
The behavior is exactly the same as with the other methods.
6. Conclusion
In this article, we’ve demonstrated how to use nested exceptions in our application and implemented a utility method to find the root cause exception. We’ve also shown how to do the same by using third-party libraries like Apache Commons Lang and Google Guava.
As always, the full source code for the examples is available over on GitHub.