Exception Handling with @RestControllerAdvice and @ExceptionHandler

In this article, we'll learn what exactly @RestControllerAdvice and @ExceptionHandler are, and how to use them in a Spring Boot REST API.
A featured image for category: Spring

1. Introduction

In this article, I would like to show you several ways we can handle exceptions with @RestControllerAdvice and @ExceptionHandler in a Spring Boot REST API.

Exceptions are nothing else than exceptional events indicating the standard program flow has been affected in any way. Sooner or later, some of them will appear in our applications and it’s our job to handle them in the best possible way.

When creating REST APIs with Spring Boot, a huge amount of work is done by the framework to prevent the application from stopping. Nevertheless, returning 500 Internal Server Error responses to clients is not the best practice and in this tutorial we will see how can we fix that.

2. @RestControllerAdvice and @ExceptionHandler

Let’s start by describing what exactly @RestControllerAdvice and @ExceptionHandler are.

@RestControllerAdvice is a syntactic sugar for @ControllerAdvice and @ResponseBody annotations. The first one, @ControllerAdvice, has been introduced in Spring 3.2 and allows us to write a code, which will be applied globally to all controllers. It can be used to add model enhancements methods, binder initialization methods, and what’s important for us today- to handle exceptions. In simple words, it allows us to implement some logic only once, instead of duplicating it across the app. Additionally, classes annotated with it do not have to be injected or declared explicitly in controllers, improving the decoupling of our logic.

On the other hand, the @ResponseBody indicates that our methods’ return values should be bound to the web response body. To put it simply- our handler methods returning some ExampleClass will be treated as ResponseEntity<ExampleClass> out of the box. As I have said, the @RestControllerAdvice combines it with the @ControllerAdvice, so that we do not have to annotate our methods explicitly.

Finally, the @ExceptionHandler is an annotation used to mark that the given method should be invoked when a specific exception is thrown.

Make a real progress thanks to practical examples, exercises, and quizzes.

Image presents a Kotlin Course box mockup for "Kotlin Handbook. Learn Through Practice"

3. Simple @ExceptionHandler

With all of that being said, let’s create the Exceptions.kt file and add exceptions classes:

class FirstCustomException(message: String) : RuntimeException(message)
class SecondCustomException(message: String) : RuntimeException(message)

@ResponseStatus(HttpStatus.BAD_REQUEST)
class ThirdCustomException(message: String) : RuntimeException(message)

class FourthCustomException(message: String) : RuntimeException(message)

As we can see, they are pretty much the same, except the ThirdCustomException, which is marked as 400 Bad Request.

As the next step, let’s create a controller class with the first method:

@RestController
class ExampleController {

  @GetMapping("/example-exception-one")
  fun getExampleExceptionOne(): ExampleResponseBody {
    throw FirstCustomException("First exception message")
    return ExampleResponseBody(message = "ok")
  }
  
  data class ExampleResponseBody(val message: String)
}

We can clearly see, that the exception is thrown and the ExampleResponseBody instance will be never returned.

Whatsoever, if we run the application and perform a GET request, we will see the Internal Server Error:

{
  "timestamp": "2022-01-14T07:08:10.061+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/example-exception-one"
}

As the next step, let’s add the ExampleAdvice class:

@RestControllerAdvice
class ExampleAdvice {

  @ExceptionHandler(FirstCustomException::class)
  fun handleFirstCustomException() {
    println("FirstCustomException handler")
  }
}

This time, the class is annotated with the @RestControllerAdvice and it contains a method which will print some text to the output. The most important thing is that the function is annotated with the @ExceptionHandler indicating, which exception should be handled by it. Moreover, it allows us to specify multiple exceptions, for example:

@ExceptionHandler(value = [ExceptionOne::class, ExceptionTwo::class])

But let’s get back to our example, rerun the application and test the endpoint once again. This time, the 200 OK response has been returned along with the desired message printed to the console:

# Console:
FirstCustomExceptionย handler
# Response:
Status: 200 OK
Body: empty

Unfortunately, the response body is empty, which may cause problems (and we will see better approaches in the next examples). Besides that, we can clearly see that the handler is working, as expected.

4. Map Status Code With @ResponseStatus

As the next example, let’s see how can we map the status code returned by our handler. Let’s add a new method within the controller:

@GetMapping("/example-exception-two")
fun getExampleExceptionTwo(): ExampleResponseBody {
  throw SecondCustomException("Second exception message")
  return ExampleResponseBody(message = "ok")
}

It’s almost exactly the same as the previous one.

Nextly, let’s implement a new exception handler:

@ResponseStatus(HttpStatus.FAILED_DEPENDENCY)
@ExceptionHandler(SecondCustomException::class)
fun handleSecondCustomException() {
  println("SecondCustomException handler")
}

This time, we’ve additionally marked it with the @ResponseStatus annotation and the desired HTTP status.

Similarly, let’s test our new endpoint:

# Console:
SecondCustomException handler
# Response:
Status: 200 OK
Body: empty

We can clearly see that the new handler has been triggered and the 424 Failed Dependency has been returned. Not an ideal solution, but still better, then the previous one.

5. Read Exception Message and Re-throw Exception

For the record, in the beginning, we’ve annotated the ThirdCustomException with @ResponseStatus:

@ResponseStatus(HttpStatus.BAD_REQUEST)
class ThirdCustomException(message: String) : RuntimeException(message)

It means, that it will be translated to 400 Bad Request each time it is thrown (so that we won’t have to mark our handler method).

Nevertheless, let’s focus on the another approach, which resolves the issue with empty response body. Just like previously, let’s start by adding the necessary controller:

@GetMapping("/example-exception-three")
fun getExampleExceptionThree(): ExampleResponseBody {
  throw ThirdCustomException("Third exception message")
  return ExampleResponseBody(message = "ok")
}

As the next step, let’s implement a new exception handler:

@ExceptionHandler(ThirdCustomException::class)
fun handleThirdCustomException(ex: ThirdCustomException) {
  println("ThirdCustomException handler. Exception message: ${ex.message}")
  throw ex
}

As we can see, except marking our methods with annotation, we additionally inject the exception instance. This approach might be really helpful when we would like to log the exception details. It’s worth mentioning, that handler methods allow us to inject plenty of information, so I highly recommend you to check out the documentation (we’ll cover one more in the next example).

Finally, let’s test the endpoint:

# Console:
ThirdCustomException handler. Exception message: Third exception message

# Response:
Status: 400 Bad Request
Body:
{
  "timestamp": "2022-01-18T01:01:01.111+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/example-exception-three"
}

As we can see, the console output contains the exception message, which might be really helpful in the real-life scenarios. Additionally, the response body is not empty anymore providing more information to the API client.

6. Return Custom Object

I’ve mentioned in the beginning that @RestControllerAdvice is a @ControllerAdvice enhanced with the @ResponseBody annotation.

Let’s add the below code:

@GetMapping("/example-exception-four")
fun getExampleExceptionFour(): ExampleResponseBody {
  throw FourthCustomException("Fourth exception message")
  return ExampleResponseBody(message = "ok")
}

As the next step, let’s implement a new handler along with a custom data class:

@ExceptionHandler(FourthCustomException::class)
fun handleFourthCustomException(
  req: HttpServletRequest,
  ex: FourthCustomException
): ExceptionResponseBody {
  println("FourthCustomException handler. Request details: [${req.getHeader("custom-header")}]")
  return ExceptionResponseBody(errorMessage = ex.message)
}

data class ExceptionResponseBody(val errorMessage: String?)

If we query this endpoint passing ‘passed value’ as a value of ‘custom-header’ header, we should see the following result:

# Console:
FourthCustomException handler. Request details: [passed value]

# Response:
Status: 200 OK
Body:
{
  "errorMessage": "Fourth exception message"
}

Unquestionably, the desired value has been obtained correctly. Moreover, the instance of ExceptionResponseBody has been serialized to JSON and sent as a request body with 200 OK status.

This strategy might be really useful, if we would like to extract some custom format for our errors.

7. @RestControllerAdvice and @ExceptionHandler Summary

And that would be all for this tutorial covering @RestControllerAdvice and @ExceptionHandler. We went through a few practical examples describing how to use these two can help you when working with Spring Boot.

If you would like to see the source code for this article, please refer to this GitHub repository. Also, I’ve created one more article related to @RestControllerAdvice, which you can find right here.

Finally, if you find this kind of articles useful, or have any suggestions, please let me know in the comments section, or through the contact form.

 

Previous articles, you might be interested in:

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy