Skip to main content

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:

  1. Controller-level exception handling: Using @ExceptionHandler annotation
  2. Global exception handling: Using @ControllerAdvice or @RestControllerAdvice
  3. HandlerExceptionResolver: For customizing exception handling at the framework level
  4. 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

java
@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:

java
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

java
@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:

java
@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:

java
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:

json
{
"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.

java
@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:

  1. Create a folder called /src/main/resources/templates/error (for Thymeleaf) or /src/main/webapp/WEB-INF/views/error (for JSP).
  2. 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:

java
@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

  1. Create specific exception classes for different error scenarios to make your code more readable and maintainable.
  2. Use appropriate HTTP status codes that accurately reflect the nature of the error.
  3. Don't expose sensitive information in error messages sent to clients.
  4. Log exceptions properly, especially for unexpected errors, to help with debugging.
  5. Provide consistent error response formats across your API.
  6. Handle validation errors separately from business logic errors.
  7. Consider internationalization for error messages in applications supporting multiple languages.
  8. 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:

  1. Controller-level exception handling using @ExceptionHandler
  2. Global exception handling with @ControllerAdvice and @RestControllerAdvice
  3. Using SimpleMappingExceptionResolver for view-based applications
  4. Custom error pages for HTTP status codes
  5. 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

Exercises

  1. Basic Exception Handling: Create a simple Spring MVC application with controller-level exception handling for at least two different exceptions.

  2. Global Exception Handler: Implement a global exception handler using @ControllerAdvice that handles at least three different types of exceptions.

  3. Custom Error Response: Design a custom error response format for a REST API and implement exception handlers that return responses in this format.

  4. Advanced Challenge: Build a Spring Boot application with a complete exception handling strategy, including custom exception classes, global handlers, and proper logging.

  5. 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! :)