Lesson 2
Error Handling in Spring Boot with Kotlin
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 in Kotlin:

Kotlin
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.PathVariable 8import org.springframework.web.bind.annotation.RestController 9 10@RestController 11class TodoController { 12 13 @GetMapping("/todos/{id}") 14 fun getTodoById(@PathVariable id: Long): Todo { 15 // Assume findTodoById(id) throws IllegalArgumentException if id is invalid 16 return findTodoById(id) 17 } 18 19 @ExceptionHandler(IllegalArgumentException::class) 20 fun handleIllegalArgumentException(ex: IllegalArgumentException): ResponseEntity<String> { 21 return ResponseEntity(ex.message, HttpStatus.BAD_REQUEST) 22 } 23}

Using @ExceptionHandler at the controller level can lead to redundancy because if multiple controllers handle the same type of exception, you might duplicate error handling logic in each controller. This approach can also result in inconsistency, as different controllers may implement slightly varied handling for the same exception, causing users to receive different error responses for similar issues, leading to an inconsistent user experience.

Global Exception Handling with @ControllerAdvice

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

Kotlin
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 10class GlobalExceptionHandler { 11 12 @ExceptionHandler(IllegalArgumentException::class) 13 fun handleIllegalArgumentException(ex: IllegalArgumentException): ResponseEntity<String> { 14 return ResponseEntity(ex.message, HttpStatus.BAD_REQUEST) 15 } 16 17 @ResponseStatus(HttpStatus.NOT_FOUND) 18 @ExceptionHandler(TodoNotFoundException::class) 19 fun handleTodoNotFoundException(ex: TodoNotFoundException): ResponseEntity<String> { 20 return ResponseEntity(ex.message, 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:

Kotlin
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") 12class TodoController { 13 14 @GetMapping("/{id}") 15 fun getTodoById(@PathVariable id: Long): Todo { 16 val todo = findTodoById(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Todo not found") 17 return todo 18 } 19}

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.yml 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."

Here’s how you can configure application.yml to always include the message:

YAML
1server: 2 error: 3 include-message: always

In the upcoming practice exercises, we'll set this property to always in the application.yml 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.