Three main methods to handle exceptions in Spring:
1. Controller level handling - @ExceptionHandler
2. Global level handling - @RestControllerAdvice
3. Method level handling - try/catch
1. Defining global exception handlers:
@RestControllerAdvice
public class GlobalExceptionHandler{
@ExceptionHandler(value = {ResourceNotFoundException.class})
public ResponseEntity<> handleResourceNotFound(ResourceNotFoundException e){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
@ExceptionHandler(value = {IllegalArgumentException.class})
public ResponseEntity<> handleBadRequest(IllegalArgumentException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid input: " + e.getMessage());
}
}
2. Centralized Error Handling:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleAllExceptions(Exception ex) {
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("timestamp", LocalDateTime.now());
errorDetails.put("message", ex.getMessage());
errorDetails.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
3. Handling Multiple Exception Types in a Single Method:
@ExceptionHandler({ResourceNotFoundException.class, IllegalArgumentException.class})
public ResponseEntity<> handleResourceNotFoundAndBadRequest(Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Error: " + e.getMessage());
}
4. Specificity and Ordering
@Slf4j
@RestControllerAdvice
public class RestApiExceptionHandler {
@ExceptionHandler(value = {Exception.class})
public ResponseEntity exception(
Exception e
){
log.error("", e);
return ResponseEntity.status(200).build();
}
@ExceptionHandler(value = {IndexOutOfBoundsException.class})
public ResponseEntity outOfBound(
IndexOutOfBoundsException e
){
log.error("IndexOutOfBoundException: " + e.getMessage());
return ResponseEntity.status(200).build();
}
}
@Slf4j
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping(path = "")
public void hello(){
var list = List.of("hello");
var element = list.get(1);
log.info("element : {}", element);
}
}
Once a request is sent to http://localhost:8080/api, the list.get(1) will throw an IndexOutOfBoundsException which will be then handled by the method defined in @RestControllerAdvice, logging: IndexOutOfBoundException: Index: 1 Size: 1
To prove that the RestApiExceptionHandler above handles exception from all classes globally, we can create a new controller, named RestApiBController as below, with a method that throws a number format exception for demonstration purposes.
@RestController
@RequestMapping("/api/b")
public class RestApiBController {
@GetMapping("/hello")
public void hello(){
throw new NumberFormatException("number format exception");
}
}

@Slf4j
@RestController
@RequestMapping("/api/b")
public class RestApiBController {
@GetMapping("/hello")
public void hello(){
throw new NumberFormatException("number format exception");
}
@ExceptionHandler(value = { NumberFormatException.class})
public ResponseEntity numberFormatException(
NumberFormatException e
){
log.error("RestApiBController: " + e.getMessage());
return ResponseEntity.status(HttpStatus.OK).build();
}
}
Once a request sent to http://localhost:8080/api/b/hello, we can see the screenshot below, showing that the exception handler defined locally was invoked instead of the one defined in the RestApiExceptionHandler (global):

We can also define a package for which the @RestControllerAdvice will handle the exception for. Once we define this in the annotation, the RestApiExceptionHandler class will only handle exception thrown from the specified package. All other exceptions thrown from other packages will not be handled by the handler class.
@Slf4j
@RestControllerAdvice(basePackages = "com.example.exception.controller")
public class RestApiExceptionHandler {
@ExceptionHandler(value = {Exception.class})
public ResponseEntity exception(
Exception e
){
log.error("{}", e.getMessage());
return ResponseEntity.status(200).build();
}
@ExceptionHandler(value = {IndexOutOfBoundsException.class})
public ResponseEntity outOfBound(
IndexOutOfBoundsException e
){
log.error("IndexOutOfBoundException: " + e.getMessage());
return ResponseEntity.status(200).build();
}
}

@Slf4j
@RestController
@RequestMapping("/api2")
public class RestApiController2 {
@GetMapping(path = "")
public void hello(){
var list = List.of("hello");
var element = list.get(1);
log.info("element : {}", element);
}
}
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IndexOutOfBoundsException: Index: 1 Size: 1] with root cause
@Slf4j
@RestControllerAdvice(basePackageClasses = {RestApiController.class, RestApiBController.class })
public class RestApiExceptionHandler {
...
}
UserResponse.java@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserResponse {
private String name;
private Integer age;
private String id;
}
Api.java@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {
private T data;
private String resultCode;
private String resultMessage;
}
Api<T> class acts as a generic response wrapper that provides a consistent, clean, and easily extensible structure for API responses. UserApiController.java@RestController
@RequestMapping("/api/user")
public class UserApiController {
private static List<UserResponse> userList = List.of(
UserResponse.builder()
.id("1")
.age(10)
.name("choi seunghwan")
.build()
,
UserResponse.builder()
.id("2")
.age(11)
.name("hong gildong")
.build()
);
@GetMapping("/id/{userId}")
public Api<UserResponse> getUser(
@PathVariable String userId
){
var user = userList.stream()
.filter(
it -> it.getId().equals(userId)
)
.findFirst()
.get();
Api<UserResponse> response = Api.<UserResponse>builder()
.resultCode(String.valueOf(HttpStatus.OK.value()))
.resultMessage(HttpStatus.OK.name())
.data(user)
.build();
return response;
}
}
Api.<UserResponse>builder():.<UserResponse> explicitly specifies the generic type parameter UserResponse for the builder being created. The period after Api is used to chain the type specification <UserResponse> with the static builder() method. builder() has no period before it?builder() method is static and belongs to the Api class. After specifying the generic type <UserResponse>, the code calls this static method directly.RestApiExceptionHandler.java: @ExceptionHandler(value = {NoSuchElementException.class})
public ResponseEntity<Api> noSuchElement(
NoSuchElementException e
){
log.error("", e);
var response = Api.builder()
.resultCode(String.valueOf(HttpStatus.NOT_FOUND.value()))
.resultMessage(HttpStatus.NOT_FOUND.getReasonPhrase())
.build();
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
http://localhost:8080/api/user/id/3 (since we do not have id 3 in our list, it should throw a NoSuchElementException):
http://localhost:8080/api/user/id/2:
We will create a GlobalExceptionHandler and remove the method that handles Exception.class from the RestApiExceptionHandler.
Reason: Separation of concerns.
- **Specific Handling in RestApiExceptionHandler
- Targeted exceptions like IndexOutOfBoundsException and NoSuchElementException have their own error-handling logic.
GlobalExceptionHandlerGlobalExceptionHandlerModified RestApiExceptionHandler:
@Slf4j
@RestControllerAdvice
@Order(1)
public class RestApiExceptionHandler {
//Removed @ExceptionHandler(value = {Exception e})
@ExceptionHandler(value = {IndexOutOfBoundsException.class})
public ResponseEntity outOfBound(
IndexOutOfBoundsException e
){
log.error("IndexOutOfBoundException: " + e.getMessage());
return ResponseEntity.status(200).build();
}
@ExceptionHandler(value = {NoSuchElementException.class})
public ResponseEntity<Api> noSuchElement(
NoSuchElementException e
){
log.error("", e);
var response = Api.builder()
.resultCode(String.valueOf(HttpStatus.NOT_FOUND.value()))
.resultMessage(HttpStatus.NOT_FOUND.getReasonPhrase())
.build();
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
}
RestApiExceptionHandler, @Order annotation was added. The value that can be specified with the @Order annotation defines the execution priority of the exception handlers. As shown in the screenshot below, the default value is Integer.MAX_VALUE.
RestApiExceptionHandler with @Order(1) and GlobalExceptionHandler with @Order (or no annotation), the RestApiExceptionHandler will have the execution priority, since the default priority constant in @Order is Integer.MAX_VALUE. GlobalExceptionHandler:@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<Api> exception(
Exception e
){
log.error("", e);
var response = Api.builder()
.resultCode(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR))
.resultMessage(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.build();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
Using the same UserApiController as above, if we send a GET request with userID = 4 which is non-existent, it should be handled by the exception(Exception e) method in the GlobalExceptionHandler since there is no method specified to handle RuntimeException in the @Order(1) RestApiExceptionHandler class. The output for the request with userId = 4 would be:

which was handled by the GlobalExceptionHandler, as shown below:

But what if there is a same generic exception(Exception e) method in the RestApiExceptionHandler class? Since the RestApiExceptionHandler is @Order(1), it has the execution priority over the GlobalExceptionHandler. Thus, the RuntimeException will be handled by the RestApiExceptionHandler.