Hello! Welcome back to your interview preparation course.
In our last lesson, we saw how to enforce data integrity using Bean Validation annotations. A key part of that process was understanding that a validation failure throws a MethodArgumentNotValidException. We briefly previewed how a @RestControllerAdvice can catch this exception to return a clean error response.
Today, we'll expand on that concept to cover all types of exceptions. Your learning outcome is to implement centralized exception handling using @ControllerAdvice. This is a hallmark of a professional, production-ready REST API. For a mid-to-senior level interview, demonstrating mastery of this topic shows you can write code that is not just functional, but also robust, maintainable, and clean.
1. From Scattered try-catch to Centralized Control
Imagine a service with multiple controller methods. Without a centralized strategy, you might handle exceptions directly inside each method:
@PostMapping("/withdraw")
public ResponseEntity<?> withdraw(@RequestBody WithdrawRequest request) {
try {
TransactionDto dto = accountService.withdraw(request.getAccountId(), request.getAmount());
return ResponseEntity.ok(dto);
} catch (AccountNotFoundException e) {
// Duplicated error handling logic
ApiError error = new ApiError("ACCOUNT_NOT_FOUND", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
} catch (InsufficientBalanceException e) {
// More duplicated logic
ApiError error = new ApiError("INSUFFICIENT_BALANCE", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}
This approach has several significant problems:
- Code Duplication: The same error-handling logic is repeated across many controllers and methods.
- Inconsistency: Different developers might create slightly different error response formats, leading to an inconsistent API.
- Cluttered Business Logic: The core purpose of the controller method is obscured by verbose error handling.
A first step to improve this is using @ExceptionHandler within the controller class. However, this only solves the problem for a single controller. The truly scalable, enterprise-grade solution is to centralize this logic for the entire application.
2. Building a Global Exception Handler with @RestControllerAdvice
Spring provides a powerful mechanism for this: @ControllerAdvice. When you annotate a class with @ControllerAdvice, you are creating a global, AOP-style interceptor that can handle exceptions from any controller.
For REST APIs, it's even better to use @RestControllerAdvice, which is a convenience annotation that combines @ControllerAdvice and @ResponseBody. This ensures that the return values from your handler methods are automatically serialized to the response body (e.g., as JSON).
A production-grade global exception handler typically has three components:
- Custom Exception Classes: For specific business rule violations (e.g.,
ResourceNotFoundException). - A Standardized Error DTO: A consistent JSON structure for all error responses.
- The Handler Class: The
@RestControllerAdviceclass containing@ExceptionHandlermethods.
Let's see how to implement these pieces.
🛡️ Mastering Global Exception Handling in Spring Boot ...
The following article, 'Mastering Global Exception Handling in Spring Boot', provides an excellent, production-focused walkthrough. Let's start by looking at how to define a custom exception and a standardized error response object.
Please read 'Step 1: Create Custom Exception Classes' and 'Step 2: Standardized Error Response DTO'. Pay attention to why we extend RuntimeException and the useful fields included in the ErrorResponse DTO, like timestamp and path.
Now that you have the building blocks—a way to signal a specific error (ResourceNotFoundException) and a structure to communicate it (ErrorResponse)—let's create the central handler itself.
🛡️ Mastering Global Exception Handling in Spring Boot ...
Now for the core component. The same article demonstrates how to build the GlobalExceptionHandler class. It shows how to handle your custom exception, the validation exception from our last lesson, and a generic fallback.
Read 'Step 3: Global Exception Handler (@ControllerAdvice)' and 'Step 4: Sample Controller'. Focus on these key points: The use of @RestControllerAdvice (or @ControllerAdvice). How each @ExceptionHandler method targets a specific exception class. How ResponseEntity is used to control both the body and the HTTP status code. The critical importance of the ExceptionHandler(Exception.class) as a safety net.
As you've seen, this pattern dramatically cleans up your controllers. They can now focus exclusively on their primary logic, throwing exceptions when rules are violated, confident that a global handler will catch them and produce a consistent, well-formed error response.
Your controller method becomes simple and readable:
@GetMapping("/products/{id}")
public String getProductById(@PathVariable int id) {
if (id != 1) {
// Just throw the exception. The GlobalExceptionHandler will take care of the rest.
throw new ResourceNotFoundException("Product with ID " + id + " not found.");
}
return "Product ID: " + id;
}
3. Fine-Tuning: Status Codes and Logging
Controlling HTTP Status Codes
You have two primary ways to associate an HTTP status code with an exception:
-
@ResponseStatuson the Exception Class:@ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { ... }This is simple and declarative. The handler method for this exception no longer needs to return a
ResponseEntity. However, it's static—aResourceNotFoundExceptionwill always result in a 404. -
Returning
ResponseEntityfrom the Handler:@ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound(...) { // ... create ErrorResponse object return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); }This is more flexible and is generally the preferred approach in complex applications. It makes the handler's behavior explicit and allows you to return different status codes based on the exception's contents if needed.
Don't Forget to Log!
A user-facing error message should be clean and simple ("Something went wrong"). However, for your internal diagnostics, you need the full story. The global exception handler is the perfect place to log the full stack trace for unexpected errors.
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// ... other handlers for specific exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex, HttpServletRequest request) {
// Log the full stack trace for developers
logger.error("An unexpected error occurred: Request URL [{}]", request.getRequestURI(), ex);
// Return a generic, safe response to the client
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
"An unexpected error occurred. Please contact support.",
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
This ensures you never leak sensitive implementation details or stack traces to the client, while still capturing all necessary information for debugging.
Test your understanding!
Your application has a business rule that prevents users from deactivating their own admin account. If this is attempted, a custom InvalidOperationException is thrown.
Write the @ExceptionHandler method for your GlobalExceptionHandler that would catch this InvalidOperationException. It should return an HTTP status of 409 Conflict and use the ErrorResponse DTO format we've discussed.
Show answer
@ExceptionHandler(InvalidOperationException.class)
public ResponseEntity<ErrorResponse> handleInvalidOperation(InvalidOperationException ex, HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.CONFLICT.value(),
"Conflict",
ex.getMessage(), // The message from the thrown exception
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
}
This handler specifically targets the InvalidOperationException, constructs a standard ErrorResponse, and correctly sets the HTTP status to 409 Conflict, clearly communicating the nature of the error to the client.
4. Preparing for the Interview
Exception handling is a frequent topic in Spring interviews. Being able to discuss these patterns clearly and confidently is key. Let's review some common questions.
Spring Boot Exception Handling – 5 Tips for Clean and ...
The article 'Spring Boot Exception Handling – 5 Tips' has a concise Q&A section that covers several classic interview questions related to this topic. Reviewing it will help solidify your understanding.
Read the section 'Spring Boot Exception Handling – Interview Questions'. Focus on the answers for: The difference between controller-level and global handlers. How to handle multiple exceptions in one method. How to return proper HTTP status codes.
These questions test your understanding of the core mechanics. A senior-level candidate would also be expected to discuss the why—the design principles of consistency, maintainability, and separation of concerns that motivate this pattern.
Conclusion
In this lesson, you've mastered a fundamental pattern for building professional REST APIs in Spring Boot. By centralizing your exception handling, you create a system that is more robust, easier to maintain, and provides a predictable and consistent experience for your API consumers.
Key Takeaways:
- Centralize with
@RestControllerAdvice: Avoidtry-catchblocks in controllers by creating a global handler. - Standardize Responses: Use a dedicated error DTO (e.g.,
ErrorResponse) to ensure all error messages have a consistent structure. - Handle Specificity: Create specific handlers for custom business exceptions (
ResourceNotFoundException), framework exceptions (MethodArgumentNotValidException), and a generic fallback forException.class. - Control the Status: Use
ResponseEntityin your handler methods to gain explicit control over the HTTP status code. - Log for Yourself, Not the User: Log detailed stack traces for unexpected errors in your handler, but return clean, safe messages to the client.
Preview of the Next Lesson:
We have now completed our module on Spring & Spring Boot Core, covering IoC, beans, validation, and exception handling. We are now ready to move to the persistence layer. In the next lesson, we will begin the "Spring Data & Transaction Management" module by exploring one of its core concepts: Explain the JPA entity lifecycle and the role of the persistence context. This will be your first step into understanding how Spring manages data and interacts with the database.