In Spring Boot, there are several ways to handle exceptions and communicate meaningful error messages to API consumers. Let us explore various approaches to error handling in Spring Boot with useful examples for each.
1. Error Handling Approaches in Spring Boot
Let us start with quickly listing all the possible approaches before going deep into each one.
Approach | Description |
---|---|
Local @ExceptionHandler | To handle specific exceptions in a controller. |
Global @ControllerAdvice | A centralized way to handle exceptions for all controllers. |
ResponseEntityExceptionHandler | Provides predefined methods for common errors such as bean validation errors and incorrect request parameters. |
@ResponseStatus for errors without body | When the response status is sufficient. |
Problem Details API (RFC 7807) | Following the industry standard way for compatibility. |
2. Using @ExceptionHandler in Controller Methods
The @ExceptionHandler
annotation is used to define a method in a controller that handles a specific exception thrown by the APIs defined in the same controller. This is why it is referred to as local exception handling.
In this following UserController class, the UserNotFoundException will be handled by the handleUserNotFoundException() method because it has been declared as @ExceptionHandler for the same exception type.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/user/{id}")
public User getUser(@PathVariable String id) {
throw new UserNotFoundException("User not found with id: " + id);
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
In the same way, we can create many such different handler methods in the controller class for different types of errors.
3. Using @ControllerAdvice for Handling Exceptions Globally
The @ControllerAdvice annotation centralizes exception handling across multiple controllers in the application. Any exception thrown from all these REST controllers are first attempted to be handled by handler methods in the @ControllerAdvice annotated class.
- Classes with @ControllerAdvice are auto-detected via classpath scanning.
- We can use selectors annotations(), basePackageClasses(), and basePackages() to define a more narrow subset of targeted controllers.
- We can apply the OR operator in the selector, i.e., a given method will be executed if any one of the given exceptions is encountered.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice(basePackages = {"com.howtodoinjava.demo.web.feature"})
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(ApplicationException.class)
public ResponseEntity<ErrorResponse> handleGenericException(ApplicationException ex) {
ErrorResponse error = new ErrorResponse("INTERNAL_SERVER_ERROR", "An unexpected error occurred.");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
4. Using ResponseEntityExceptionHandler for Common Exceptions
The ResponseEntityExceptionHandler is a convenient base class for @ControllerAdvice classes that wish to provide centralized exception handling across all @RequestMapping methods. It includes default methods to handle:
- Bean validation errors
- HTTP request errors
- Uniform responses for common exceptions handled internally by Spring
We can override the provided default methods to further customize the behavior of error responses.
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
private String INCORRECT_REQUEST = "INCORRECT_REQUEST";
private String BAD_REQUEST = "BAD_REQUEST";
@ExceptionHandler(RecordNotFoundException.class)
public final ResponseEntity<ErrorResponse> handleUserNotFoundException
(RecordNotFoundException ex, WebRequest request) {
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
ErrorResponse error = new ErrorResponse(INCORRECT_REQUEST, details);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MissingHeaderInfoException.class)
public final ResponseEntity<ErrorResponse> handleInvalidTraceIdException
(MissingHeaderInfoException ex, WebRequest request) {
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
ErrorResponse error = new ErrorResponse(BAD_REQUEST, details);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
Note that we should Extend ResponseEntityExceptionHandler only if we want to customize the behavior for predefined exceptions. Although we can use ResponseEntityExceptionHandler without using @ControllerAdvice, combining both allows to handle other custom exceptions globally.
5. Using @ResponseStatus for Mapping Exceptions to Status Codes
In Spring Boot, the @ResponseStatus annotation associates a specific HTTP status code with an exception or controller method. This is particularly useful when we want to simplify exception handling for very specific usecases by directly mapping exceptions to HTTP status codes such as 404 for “Not Found” or 400 for “Bad Request”.
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "User not found")
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
When this exception is thrown from an API method, Spring automatically responds with a 404 status code and the message “User not found.”
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable String id) {
//...
throw new UserNotFoundException("User not found with ID: " + id);
}
}
Note that this type of exception handling is static and does not support dynamic messages. If we need dynamic content, use a custom exception handler instead.
6. Problem Detail API for Industry Standard Formats
Starting from Spring 6 and Spring Boot 3, the Spring Framework supports the “Problem Details for HTTP APIs” specification, RFC 7807. This RFC defines simple JSON and XML document formats that can be used to communicate the problem details to the API consumers. This is useful in cases where HTTP status codes are not enough to describe the problem with an HTTP API.
The following is an example of a problem that occurred while transferring from one bank account to another, and we do not have sufficient balance in our account.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"status": 403,
"type": "https://bankname.com/common-problems/low-balance",
"title": "You not have enough balance",
"detail": "Your current balance is 30 and you are transterring 50",
"instance": "/account-transfer-service"
}
We can combine the @ControllerAdvice annotation with ProblemDetail API to create exception classes that are closer to their business domain/model. And then we can handle these exceptions to return the error messages in industry acceptable ways.
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Value("${hostname}")
private String hostname;
@ExceptionHandler(RecordNotFoundException.class)
public ProblemDetail handleRecordNotFoundException(RecordNotFoundException ex, WebRequest request) {
ProblemDetail body = ProblemDetail
.forStatusAndDetail(HttpStatusCode.valueOf(404),ex.getLocalizedMessage());
body.setType(URI.create("http://my-app-host.com/errors/not-found"));
body.setTitle("Record Not Found");
body.setProperty("hostname", hostname);
return body;
}
}
You can read more about Problem Detail API in the linked post.
7. Summary
In this Spring Boot error handling tutorial, we learned different ways to handle various types of exceptions. No approach is best and fits all scenarios. We must choose an approach based on its specific purpose and application requirements.
Happy Learning !!
Comments