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.
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 @ExceptionHandler
annotation allows you to handle exceptions at the controller level. Here’s a code snippet demonstrating how to use it in Kotlin:
Kotlin1package 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.
For a more centralized approach, Spring Boot offers @ControllerAdvice
. This allows you to handle exceptions globally across all controllers:
Kotlin1package 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.
Another approach is using ResponseStatusException
, a programmatic way to throw HTTP status exceptions directly from your code:
Kotlin1package 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.
When you send a GET HTTP request to the /todos/invalid-id
endpoint now, you'll notice that the response might look like this:
JSON1{ 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 parametermessage=true
. Otherwise, the response won't include the "message."
Here’s how you can configure application.yml
to always include the message:
YAML1server: 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.
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.
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!