Lesson 2
Exception Handling in Spring Boot
Introduction

Welcome! In our previous lesson, we delved into the intricacies of data validation within Spring Boot REST APIs. Today, we're addressing a vital aspect of building resilient APIs: error handling. Proper error handling ensures that users receive meaningful feedback and that your application does not expose unnecessary details when something goes wrong.

Understanding Error Handling in Spring Boot

Error handling in Spring Boot is critical for translating cryptic 500 Internal Server Error responses into more informative HTTP responses with appropriate status codes and messages. There are multiple approaches to handling errors in Spring Boot, each with its own use cases:

  • @ExceptionHandler: This annotation allows you to handle exceptions at the controller level, providing fine-grained control over error handling within individual controllers.
  • @ControllerAdvice: This annotation enables global exception handling across all controllers, promoting consistency and reusability of error handling logic throughout your application.
  • ResponseStatusException: This is a programmatic way to throw HTTP status exceptions directly from your code, making it easy to return specific HTTP status codes and messages from within your controller methods.

Let's explore each of these methods in more detail.

The Controller-Level @ExceptionHandler

The @ExceptionHandler annotation allows you to handle exceptions at the controller level. Here’s a code snippet demonstrating how to use it:

Java
1package com.codesignal; 2 3import org.springframework.http.HttpStatus; 4import org.springframework.http.ResponseEntity; 5import org.springframework.web.bind.annotation.ExceptionHandler; 6import org.springframework.web.bind.annotation.GetMapping; 7import org.springframework.web.bind.annotation.RestController; 8 9@RestController 10public class TodoController { 11 12 @GetMapping("/todos/{id}") 13 public Todo getTodoById(@PathVariable Long id) { 14 // Assume findTodoById(id) throws IllegalArgumentException if id is invalid 15 return findTodoById(id); 16 } 17 18 @ExceptionHandler(IllegalArgumentException.class) 19 public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) { 20 return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); 21 } 22}

Using @ExceptionHandler at the controller level is quick and easy, but it can lead to redundancy and inconsistency if multiple controllers need to handle the same types of exceptions.

Global Exception Handling with @ControllerAdvice

For a more centralized approach, Spring Boot offers @ControllerAdvice. This allows you to handle exceptions globally across all controllers:

Java
1package com.codesignal; 2 3import org.springframework.http.HttpStatus; 4import org.springframework.http.ResponseEntity; 5import org.springframework.web.bind.annotation.ControllerAdvice; 6import org.springframework.web.bind.annotation.ExceptionHandler; 7import org.springframework.web.bind.annotation.ResponseStatus; 8 9@ControllerAdvice 10public class GlobalExceptionHandler { 11 12 @ExceptionHandler(IllegalArgumentException.class) 13 public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) { 14 return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); 15 } 16 17 @ResponseStatus(HttpStatus.NOT_FOUND) 18 @ExceptionHandler(TodoNotFoundException.class) 19 public ResponseEntity<String> handleTodoNotFoundException(TodoNotFoundException ex) { 20 return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); 21 } 22}

@ControllerAdvice ensures consistency and reusability by centralizing error handling logic. However, it could make your error handling logic a bit less straightforward to track, especially for new developers joining the project.

Using ResponseStatusException

Another approach is using ResponseStatusException, a programmatic way to throw HTTP status exceptions directly from your code:

Java
1package com.codesignal; 2 3import org.springframework.http.HttpStatus; 4import org.springframework.web.bind.annotation.GetMapping; 5import org.springframework.web.bind.annotation.PathVariable; 6import org.springframework.web.bind.annotation.RequestMapping; 7import org.springframework.web.bind.annotation.RestController; 8import org.springframework.web.server.ResponseStatusException; 9 10@RestController 11@RequestMapping("/todos") 12public class TodoController { 13 14 @GetMapping("/{id}") 15 public Todo getTodoById(@PathVariable Long id) { 16 Todo todo = findTodoById(id); 17 if (todo == null) { 18 throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Todo not found"); 19 } 20 return todo; 21 } 22}

ResponseStatusException is simple and direct, which makes it easy to use for straightforward cases. However, it may not be as flexible or reusable as other approaches and requires explicit handling in the controller logic.

server.error.include-message Property

When you send a GET HTTP request to the /todos/invalid-id endpoint now, you'll notice that the response might look like this:

JSON
1{ 2 "timestamp": "2024-08-10T12:00:00.000+00:00", 3 "status": 404, 4 "error": "Not Found", 5 "path": "/todos/invalid-id" 6}

However, the message "Todo not found" that you specified isn't included. This is because the application property server.error.include-message is not set. By default, Spring doesn't return error messages to reduce the risk of exposing sensitive information.

To control whether the "message" field is included in error responses, you should set the server.error.include-message property in the application.properties file. This property supports three values:

  • always – The "message" field is always included in error responses.
  • never – The "message" field is never included in error responses.
  • on_param – The "message" field is included in error responses only if the request contains the parameter message=true. Otherwise, the response won't include the "message".

In the upcoming practice exercises, we'll set this property to always in the application.properties file to ensure the message is included in the response.

Rule of Thumb

Understanding where exceptions can happen and the scope of their impact can guide you in choosing the right error handling technique:

  • If an exception is specific to a method, use ResponseStatusException. This allows you to throw HTTP status exceptions directly from your code for straightforward case handling.
  • If an exception is specific to a controller, use @ExceptionHandler at the controller level. This approach is quick and easy for handling exceptions localized to a single controller.
  • If an exception can happen across multiple controllers, use @ControllerAdvice. This provides a centralized, reusable way to handle exceptions globally across all controllers.
Summary

We've covered essential error-handling techniques in Spring Boot, from @ExceptionHandler for controller-specific handling to @ControllerAdvice for global error management, and ResponseStatusException for in-code status handling. Each method has its strengths and is suited for different scenarios. Up next, you'll practice these concepts to ensure a solid understanding. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.