Spring MVC Exception Handling
Introduction
Exception handling is a critical aspect of building robust and user-friendly web applications. In Spring MVC, handling exceptions properly ensures that your application can gracefully manage errors and present appropriate feedback to users when things go wrong.
Without proper exception handling, your application might crash or display cryptic error messages that confuse users and expose sensitive information. Spring MVC provides several elegant approaches to handle exceptions that occur during request processing.
In this tutorial, we'll explore different techniques for exception handling in Spring MVC applications, from simple approaches to more sophisticated strategies.
Understanding Exceptions in Spring MVC
When an exception occurs in a Spring MVC application, the default behavior is to propagate the exception up the call stack until it reaches the DispatcherServlet, which typically returns an HTTP 500 (Internal Server Error) response to the client.
However, Spring MVC provides several mechanisms to intercept these exceptions and handle them appropriately:
- Controller-level exception handling: Using
@ExceptionHandler
annotation - Global exception handling: Using
@ControllerAdvice
or@RestControllerAdvice
- HandlerExceptionResolver: For customizing exception handling at the framework level
- SimpleMappingExceptionResolver: For mapping exceptions to specific views
Let's explore each of these approaches in detail.
Controller-Level Exception Handling with @ExceptionHandler
The simplest way to handle exceptions in Spring MVC is by using the @ExceptionHandler
annotation within your controller. This approach limits the scope of exception handling to that specific controller.
Example: Basic Exception Handler in a Controller
@Controller
public class ProductController {
@GetMapping("/product/{id}")
public String getProduct(@PathVariable("id") Long id) {
// Logic that might throw exceptions
if (id == null) {
throw new IllegalArgumentException("Product ID cannot be null");
}
if (id <= 0) {
throw new ProductNotFoundException("Product not found with id: " + id);
}
// Normal processing...
return "productDetail";
}
@ExceptionHandler(ProductNotFoundException.class)
public ModelAndView handleProductNotFoundException(ProductNotFoundException ex) {
ModelAndView modelAndView = new ModelAndView("error/product-not-found");
modelAndView.addObject("message", ex.getMessage());
modelAndView.setStatus(HttpStatus.NOT_FOUND);
return modelAndView;
}
@ExceptionHandler(IllegalArgumentException.class)
public ModelAndView handleIllegalArgumentException(IllegalArgumentException ex) {
ModelAndView modelAndView = new ModelAndView("error/bad-request");
modelAndView.addObject("message", ex.getMessage());
modelAndView.setStatus(HttpStatus.BAD_REQUEST);
return modelAndView;
}
}
In this example:
- We have a controller that may throw exceptions while processing requests
- We defined two exception handlers using
@ExceptionHandler
to catch specific exceptions - Each handler method returns a specific view with a customized error message and HTTP status
Custom Exception Classes
Creating your own exception classes makes your code more readable and allows for more specific handling:
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}
Global Exception Handling with @ControllerAdvice
While controller-level exception handling works well for exceptions thrown within a single controller, it doesn't help with exceptions thrown in other controllers or outside the controller layer.
For application-wide exception handling, Spring MVC provides the @ControllerAdvice
annotation (or @RestControllerAdvice
for REST applications).
Example: Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ModelAndView handleProductNotFoundException(ProductNotFoundException ex) {
ModelAndView modelAndView = new ModelAndView("error/product-not-found");
modelAndView.addObject("message", ex.getMessage());
modelAndView.setStatus(HttpStatus.NOT_FOUND);
return modelAndView;
}
@ExceptionHandler(Exception.class)
public ModelAndView handleGenericException(Exception ex) {
ModelAndView modelAndView = new ModelAndView("error/generic-error");
modelAndView.addObject("message", "An unexpected error occurred");
modelAndView.addObject("details", ex.getMessage());
modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
return modelAndView;
}
}
This global exception handler will catch exceptions thrown from any controller in your application. The @ExceptionHandler(Exception.class)
method works as a catch-all for any unhandled exceptions.
REST API Exception Handling
For REST APIs, you'll typically want to return JSON responses instead of views. Using @RestControllerAdvice
makes this simple:
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleProductNotFoundException(ProductNotFoundException ex) {
return new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
System.currentTimeMillis()
);
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ex) {
return new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
System.currentTimeMillis()
);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGenericException(Exception ex) {
return new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"An unexpected error occurred",
System.currentTimeMillis()
);
}
}
And here's the ErrorResponse
class:
public class ErrorResponse {
private int status;
private String message;
private long timestamp;
// Constructor, getters, and setters
public ErrorResponse(int status, String message, long timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
// Getters and setters...
}
When an exception occurs, this handler will return a JSON response like:
{
"status": 404,
"message": "Product not found with id: 123",
"timestamp": 1632145028776
}
Using SimpleMappingExceptionResolver
For traditional Spring MVC applications that render views, you can also configure a SimpleMappingExceptionResolver
bean to map exceptions to specific view names.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public SimpleMappingExceptionResolver exceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("ProductNotFoundException", "error/product-not-found");
mappings.setProperty("IllegalArgumentException", "error/bad-request");
mappings.setProperty("Exception", "error/generic-error");
resolver.setExceptionMappings(mappings);
resolver.setDefaultErrorView("error/default");
resolver.setExceptionAttribute("exception");
return resolver;
}
}
This configuration will map specific exceptions to different views without requiring any @ExceptionHandler
methods.
Custom Error Pages for HTTP Status Codes
Spring Boot provides a simple way to create custom error pages for specific HTTP status codes. Simply place your error pages in the appropriate location:
- Create a folder called
/src/main/resources/templates/error
(for Thymeleaf) or/src/main/webapp/WEB-INF/views/error
(for JSP). - Add error pages named after the status code (e.g.,
404.html
,500.html
).
Spring Boot will automatically use these pages for the corresponding HTTP status codes.
Real-World Application: E-Commerce System
Let's see how we might implement exception handling in a more complex real-world e-commerce application:
@RestControllerAdvice
public class ECommerceExceptionHandler {
// Business logic exceptions
@ExceptionHandler(OutOfStockException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ErrorResponse handleOutOfStockException(OutOfStockException ex) {
return new ErrorResponse(
"INVENTORY_ERROR",
ex.getMessage(),
HttpStatus.CONFLICT.value()
);
}
@ExceptionHandler(PaymentFailedException.class)
@ResponseStatus(HttpStatus.PAYMENT_REQUIRED)
public ErrorResponse handlePaymentFailedException(PaymentFailedException ex) {
return new ErrorResponse(
"PAYMENT_ERROR",
ex.getMessage(),
HttpStatus.PAYMENT_REQUIRED.value()
);
}
// Validation exceptions
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return new ValidationErrorResponse(
"VALIDATION_ERROR",
"Validation failed for the request",
HttpStatus.BAD_REQUEST.value(),
errors
);
}
// Security exceptions
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorResponse handleAccessDeniedException(AccessDeniedException ex) {
return new ErrorResponse(
"SECURITY_ERROR",
"You don't have permission to access this resource",
HttpStatus.FORBIDDEN.value()
);
}
// Catch-all handler
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleAllUncaughtExceptions(Exception ex) {
// Log the exception details for debugging
logger.error("Unexpected error occurred", ex);
return new ErrorResponse(
"SYSTEM_ERROR",
"An unexpected error occurred. Please contact support.",
HttpStatus.INTERNAL_SERVER_ERROR.value()
);
}
}
This handler covers various scenarios an e-commerce application might face:
- Business logic exceptions (inventory, payment)
- Validation failures for user input
- Security-related exceptions
- Unexpected errors with proper logging
Best Practices for Exception Handling
- Create specific exception classes for different error scenarios to make your code more readable and maintainable.
- Use appropriate HTTP status codes that accurately reflect the nature of the error.
- Don't expose sensitive information in error messages sent to clients.
- Log exceptions properly, especially for unexpected errors, to help with debugging.
- Provide consistent error response formats across your API.
- Handle validation errors separately from business logic errors.
- Consider internationalization for error messages in applications supporting multiple languages.
- Test your exception handlers to ensure they behave as expected.
Summary
In this tutorial, we've explored various approaches to exception handling in Spring MVC:
- Controller-level exception handling using
@ExceptionHandler
- Global exception handling with
@ControllerAdvice
and@RestControllerAdvice
- Using
SimpleMappingExceptionResolver
for view-based applications - Custom error pages for HTTP status codes
- A comprehensive real-world implementation for a complex application
Proper exception handling is essential for building robust and user-friendly applications. By implementing these patterns, you can ensure that your application gracefully manages errors and provides appropriate feedback to users when things go wrong.
Additional Resources and Exercises
Additional Resources
- Spring MVC Exception Handling Documentation
- Spring Boot Error Handling Guide
- Baeldung: Error Handling for REST with Spring
Exercises
-
Basic Exception Handling: Create a simple Spring MVC application with controller-level exception handling for at least two different exceptions.
-
Global Exception Handler: Implement a global exception handler using
@ControllerAdvice
that handles at least three different types of exceptions. -
Custom Error Response: Design a custom error response format for a REST API and implement exception handlers that return responses in this format.
-
Advanced Challenge: Build a Spring Boot application with a complete exception handling strategy, including custom exception classes, global handlers, and proper logging.
-
Error Page Customization: Create custom error pages for common HTTP status codes (404, 403, 500) and test them by triggering these errors in your application.
By mastering exception handling in Spring MVC, you'll be able to build more robust and user-friendly applications that gracefully handle errors and provide appropriate feedback to users.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)