Customized Exception Handling in Spring Boot
By: Amal Amine
Tags: Java, Spring Boot, Debugging, Programming, Web Development

Exceptions are one of many structures that maintain the control flow of a running application. More specifically, an exception is an event that would disturb the otherwise normal flow of a program. When writing code, we attempt to predict where or when an exception could occur, and explicitly define new instructions for our application to follow if it does encounter one— this process is known as exception handling.

Exception handling in Java is done mainly through try catch blocks, along with throw and throws declarations.

The main objective of exception handling, in Java or otherwise, is to maintain the normal flow of the application; when an object is thrown at runtime due to an unexpected abnormal event, we want to limit the repercussions and keep our application alive and running. We also to bring to our attention that a problem occurred, in hopes that we are able to prevent future occurrences of the same issue.

It’s hard to resolve the issue, however, if we’re looking at a humongous stack trace that tells us close to nothing. Which entails going beyond try, catch, finally, throw and throws into creating custom user-defined exceptions and global exception handlers to intercept them.

Let’s demonstrate with an example.

We’ll create an API endpoint that takes a passcode, and returns the address of a secret door.

Controller for our secret door endpoint

Schema for the response returned by the secret door endpoint

Running the endpoint should return the correct response:

Successful response returned

Now, we’ll add in validation for the passcode — those who don’t know the correct secret word, shouldn’t get the address.

Error response returned

Let’s take a closer look at the response:

{

"timestamp": 1561571053912,

"status": 500,

"error": "Internal Server Error",

"message": "Wrong passcode!",

"trace": "java.lang.Exception: Wrong passcode!

\tat fn.service.core.controllers.health.SecretDoor.secretDoorBouncer(SecretDoor.java:14)

\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

\tat....<50 more lines of incomprehensible stack trace>,

"path": "/secret-door"

}

We managed to slip through a somewhat descriptive message, but those using our API will need more help. What if we wanted to include a hint, possible next actions, and pointers at where they could find support for their issue?

Perhaps throwing a generic exception wasn’t a good idea.

Let’s start by creating our own custom exception.

Surely we can do better. Let’s create a new exception, with the correct schema and response format.

Schema for our custom exception

Custom exception which extends RuntimeException

Now, instead of throwing a generic exception, we can throw the CustomException we created.

Now that we’ve included plenty of details, let’s hit the endpoint again.

Error response returned when a CustomException is thrown

Looks like we’re not quite there yet — our CustomException is being intercepted by Java’s ResponseEntityExceptionHandler , which handles all runtime exceptions and returns a formatted ResponseEntity .

Let’s intercept those exceptions to force our custom format.

We need to override the ResponseEntityExceptionHandler and return the correctly formatted ResponseEntity by creating a custom ExceptionInterceptor .

To achieve that, we made use of the following annotations:

@ControllerAdvice : allows our class to be a global interceptor of exceptions thrown by methods annotated by @RequestMapping .

: allows our class to be a global interceptor of exceptions thrown by methods annotated by . @ExceptionHandler : declares that our handleAllExceptions method will handle any exception of the type CustomException .

This way we can return exceptions in the custom exception response schema we defined.

Our helpful custom exception in action

Let’s test the exception interceptor we wrote.

No new feature is complete without a test to ensure we won’t break it as we add more custom exceptions and custom exception handlers.

To test our global exception handler, we wrote a simple unit test to ensure that it returns the correct schema with Mockito & JUnit.

What else can we do?

In this example we decided to override all RuntimeException occurrences. While throwing our newly created CustomException was less generic than throwing an Exception , it still did not provide enough specificity. a RuntimeException can be, for example, an ArithmeticException , a NullPointerException , a NumberFormatException , or an IndexOutOfBoundsException . It would be a good idea to take some time to understand the hierarchy of Java Exception classes and Java exception handling to ensure that we’re overriding the correct exception class when writing user-defined exceptions.

With our newly acquired knowledge, we should be able to create descriptive, precise and helpful user-defined exceptions that help us keep our program running, and our sanity in check.