Java Notes
Complete guide to exception handling in Spring Boot - @ExceptionHandler, @ControllerAdvice, custom exceptions, error responses, and validation error handling.
Why Proper Exception Handling?
Without proper handling, Spring Boot returns default error responses that expose internal details:
{
"timestamp": "2024-03-15T10:30:00",
"status": 500,
"error": "Internal Server Error",
"trace": "java.lang.NullPointerException at com.example...",
"path": "/api/users/999"
}With proper handling, you return clean, consistent, secure responses:
{
"success": false,
"status": 404,
"message": "User not found with id: 999",
"timestamp": "2024-03-15T10:30:00"
}Standard Error Response
@Data
@Builder
public class ErrorResponse {
private boolean success;
private int status;
private String message;
private String path;
private LocalDateTime timestamp;
private Map<String, String> errors; // For validation errors
}Global Exception Handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Handle specific custom exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex, HttpServletRequest request) {
log.warn("Resource not found: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.success(false)
.status(HttpStatus.NOT_FOUND.value())
.message(ex.getMessage())
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(DuplicateResourceException.class)
public ResponseEntity<ErrorResponse> handleDuplicate(
DuplicateResourceException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.builder()
.success(false)
.status(HttpStatus.CONFLICT.value())
.message(ex.getMessage())
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
}
// Handle validation errors (@Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex, HttpServletRequest request) {
Map<String, String> fieldErrors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
fieldErrors.put(error.getField(), error.getDefaultMessage()));
ErrorResponse error = ErrorResponse.builder()
.success(false)
.status(HttpStatus.BAD_REQUEST.value())
.message("Validation failed")
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.errors(fieldErrors)
.build();
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
// Handle type mismatch (e.g., string passed as Long)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(
MethodArgumentTypeMismatchException ex, HttpServletRequest request) {
String message = String.format("Parameter '%s' should be of type %s",
ex.getName(), ex.getRequiredType().getSimpleName());
ErrorResponse error = ErrorResponse.builder()
.success(false)
.status(HttpStatus.BAD_REQUEST.value())
.message(message)
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
// Handle missing request parameters
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ErrorResponse> handleMissingParams(
MissingServletRequestParameterException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.builder()
.success(false)
.status(HttpStatus.BAD_REQUEST.value())
.message("Required parameter missing: " + ex.getParameterName())
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
// Handle HTTP method not supported
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ErrorResponse> handleMethodNotAllowed(
HttpRequestMethodNotSupportedException ex, HttpServletRequest request) {
ErrorResponse error = ErrorResponse.builder()
.success(false)
.status(HttpStatus.METHOD_NOT_ALLOWED.value())
.message("Method " + ex.getMethod() + " not supported. Supported: "
+ ex.getSupportedHttpMethods())
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return new ResponseEntity<>(error, HttpStatus.METHOD_NOT_ALLOWED);
}
// Catch-all for unexpected errors
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(
Exception ex, HttpServletRequest request) {
log.error("Unexpected error at {}: {}", request.getRequestURI(), ex.getMessage(), ex);
ErrorResponse error = ErrorResponse.builder()
.success(false)
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("An unexpected error occurred. Please try again later.")
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}Using Exceptions in Service Layer
@Service
public class UserService {
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
}
public User createUser(UserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateResourceException(
"User with email " + request.getEmail() + " already exists");
}
// create user...
}
public void changePassword(Long userId, String oldPassword, String newPassword) {
User user = getUserById(userId);
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new BadRequestException("Current password is incorrect");
}
if (newPassword.length() < 8) {
throw new BadRequestException("New password must be at least 8 characters");
}
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
}Exception Handling Best Practices
| Practice | Reason |
|---|---|
| Use custom exceptions | Clear, specific error types |
| Return consistent error format | Client can parse predictably |
| Never expose stack traces in production | Security risk |
| Log errors server-side | Debugging without exposing to client |
| Use appropriate HTTP status codes | RESTful semantics |
| Handle validation errors separately | Return field-level errors |
| Catch specific exceptions before generic | More precise handling |
Interview Key Points
@RestControllerAdvice=@ControllerAdvice+@ResponseBody@ExceptionHandlercatches exceptions thrown from controllers- Custom exceptions should extend
RuntimeException(unchecked) @ResponseStatuscan annotate exceptions directly for simple cases- Validation errors come from
MethodArgumentNotValidException - Always return consistent error response format across all endpoints
- Log exceptions server-side; return sanitized messages to client
- Order matters: specific exception handlers take priority over generic
Summary
In this chapter, we learned about Exception Handling in Spring Boot in detail. Here are the key points:
- Basic introduction to the concept and why it is needed
- Syntax and structure with complete examples
- Internal working and best practices
- Real-world applications and use cases
- Common mistakes and how to avoid them
- How this topic is asked in interviews
To practice these concepts, write and run the code in your IDE and verify the output. Modify each example and experiment so that you deeply understand the concept.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Exception Handling in Spring Boot.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, spring, framework, boot
Related Java Master Course Topics