Spring Boot 3 Crash Course Part 5: Exception Handling

Ben Meehan
5 min readNov 26, 2023

--

Exception handling is a crucial aspect of any application, and in the context of Spring Boot, it becomes even more important. Spring Boot provides a powerful and flexible way to handle exceptions using the @ControllerAdvice annotation.

@ControllerAdvice is an annotation provided by Spring that allows you to write global code that can be applied to a wide range of controllers. This is particularly useful for handling exceptions across your entire application.

Link to part 4

Part 6 is now available Here

Table of Contents:

  1. Why Do We Need Separate Exception Handling?
  2. Creating Custom Exception (Optional)
  3. Creating Custom Error Response
  4. Throwing An Exception
  5. Handling The Thrown Exception

Why Do We Need Separate Exception Handling?

When a client sends a request and an error occurs, it’s a common practice to handle this scenario gracefully and provide a client-friendly response instead of a generic internal server error.

This helps improve the user experience and allows clients to better understand and handle these situations.

We do not need to send the whole error that Spring threw to the client. For one, it would be pointless and two it just creates more mess on the client side.

Create a Spring Boot Project:

Start by creating a new Spring Boot project. You can use Spring Initializr (https://start.spring.io/) to generate a basic project structure as we have done before. Include the “Spring Web” dependency.

Say we are building a library application and want to handle the errors that occur.

Creating Custom Exception (Optional):

This step is optional but recommended for a more organized exception handling approach. To create a custom exception class that will be thrown in your application instead of a built-in exception,

Create a CustomException.java file with the following

public class BookNotFoundException extends RuntimeException {

private final String errorCode;

public BookNotFoundException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}

public String getErrorCode() {
return errorCode;
}
}

The custom exception class must inherit the RuntimeException class. Inside the constructor, we need to call the super class constructor with an error message string.

Creating Custom Error Response:

In Spring Boot, a common practice is to return a custom error response that is tuned for the client’s needs. This custom response can include only the fields you want to send like status code and error message.

Create an another file called CustomErrorResponse.java with the following

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class CustomErrorResponse {

private String errorCode;
private String message;
private String timestamp;


public CustomErrorResponse(String errorCode, String message) {
this.errorCode = errorCode;
this.message = message;
this.timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
}

// Other constructors, getters, and setters
}

In this response, we send over the HTTP status code, the error message and the timestamp when it happened.

Throwing An Exception:

Now we need to throw an exception whenever something bad happens in our controller.

Create a Controller.java file with the following

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class DemoController {

@GetMapping("/books/{id}")
public String getBook(@PathVariable Long id) {
// Simulate database lookup
// Replace this with your actual data access logic
if (id.equals(123L)) {
throw new BookNotFoundException("BOOK_NOT_FOUND", "Book with ID " + id + " not found");
} else {
// Return book data
return "Book data for ID: " + id;
}
}
}

In here, I am manually throwing the custom exception we created.

Handling The Thrown Exception:

To handle the thrown exception, we will use @ControllerAdvice annotation.

@ControllerAdvice annotation in Spring is somewhat analogous to middleware in its purpose, though the terminology differs.

controller advice is similar to a middleware

In simple terms, @ControllerAdvice in Spring is like a superhero that comes to the rescue when something goes wrong in your web application. It's there to handle problems and make sure everything stays under control.

Imagine you have a bunch of controllers in your application, each responsible for handling different requests. Now, sometimes things may not go as planned — maybe a user requests data that doesn’t exist or submits invalid information. This is where @ControllerAdvice steps in.

@RestControllerAdvice is a specialization of the @ControllerAdvice annotation. It combines the functionality of @ControllerAdvice and @ResponseBody, making it suitable for RESTful services.

Create a new file called ControllerAdvice.java with the following

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<CustomErrorResponse> handleBookNotFoundException(BookNotFoundException ex) {
CustomErrorResponse response = new CustomErrorResponse(ex.getErrorCode(), ex.getMessage());
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}

// Add more exception handlers as needed
}

Here is an explanation of the annotations in code.

@RestControllerAdvice:

  • This annotation tells Spring that the class is designed to provide centralized exception handling for RESTful controllers.
  • It combines @ControllerAdvice and @ResponseBody, indicating that it will handle exceptions and send the response directly in a RESTful format (like JSON).

@ExceptionHandler(BookNotFoundException.class):

  • This annotation is used on a method to declare that it will handle exceptions of the specified type (BookNotFoundException in this case).
  • When a BookNotFoundException occurs in any controller method, this method will be invoked to handle it.

@ResponseStatus(HttpStatus.NOT_FOUND):

  • This annotation specifies the HTTP response status code to be set when the exception is handled. In this case, it’s set to 404 (NOT FOUND) because the exception is a BookNotFoundException.

Finally,

public ResponseEntity<CustomErrorResponse> handleBookNotFoundException(BookNotFoundException ex):

  • This method is the actual exception handler.
  • It takes the specific exception (BookNotFoundException) as a parameter, allowing it to access details about the exception, such as the error code and message.

Now, if a client sends a request to /api/books/123 (assuming the book with ID 123 is not found), the BookNotFoundException will be thrown. The GlobalExceptionHandler will catch this exception and return a JSON response using the CustomErrorResponse class with a 404 status.

That’s it! That is error handling in Spring Boot. It may sound like a lot of work just to handle exceptions, but staying organized with advices will help you in the long run; especially in large projects where there are hundreds of controllers.

Have fun learning Spring Boot!

“Time flies over us, but leaves its shadow behind” — Nathaniel Hawthorne

--

--

Ben Meehan

Software Engineer at Razorpay. Sharing knowledge and experiences.