Introduction
Robust exception management is critical for any web application. Without a centralized strategy, business logic becomes clutttered with repetitive error-handling code. Spring MVC provides multiple mechanisms to manage exceptions consistently across your application.
Method 1: Global Exception Handling with @ControllerAdvice
The most effective approach leverages @ControllerAdvice to create a dedicated exception handler that intercepts errors from all controllers.
Basic View-Based Exception Handler
package com.example.app.error;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ControllerAdvice
public class ApplicationExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
logErrorDetails(exception);
ModelAndView modelView = new ModelAndView();
modelView.setViewName("error");
return modelView;
}
private void logErrorDetails(Exception exception) {
StringBuilder errorMessage = new StringBuilder();
errorMessage.append("Unhandled Exception Occurred\n");
errorMessage.append(exception.getClass().getCanonicalName());
if (exception.getMessage() != null) {
errorMessage.append(": ").append(exception.getMessage());
}
for (StackTraceElement element : exception.getStackTrace()) {
errorMessage.append("\n\tat ").append(element);
}
LOGGER.error(errorMessage.toString(), exception);
}
}
JSON Response for REST APIs
For RESTful services, return structured eror data by adding @ResponseBody:
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleExceptionAsJson(Exception exception) {
logErrorDetails(exception);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", System.currentTimeMillis());
errorResponse.put("status", "error");
errorResponse.put("message", "An unexpected error occurred");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
Configuration and DispatcherServlet Behavior
The @ControllerAdvice annotation includes @Component, enabling automatic component scanning. When an unmapped URL is requested, the global handler may not trigger because DispatcherServlet invokes noHandlerFound() which, by default, sends a 404 response without throwing an exception.
To enable exception propagation for unmapped URLs, set throwExceptionIfNoHandlerFound to true:
Java Configuration Approach
package com.example.app.config;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{RootConfiguration.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{WebConfiguration.class};
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
boolean parameterSet = registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
if (!parameterSet) {
throw new IllegalStateException("Failed to configure DispatcherServlet");
}
}
}
Alternative: Direct Servlet Configuration
@Override
protected void registerDispatcherServlet(ServletContext servletContext) {
super.registerDispatcherServlet(servletContext);
FrameworkServlet servlet = (FrameworkServlet) servletContext.getAttribute(getServletName());
servlet.setThrowExceptionIfNoHandlerFound(true);
}
XML Configuraton Approach
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
Static Resource Handling Conflict
When throwExceptionIfNoHandlerFound is enabled, you must disable the default static resource servlet to prevent it from intercepting unmapped requests. Remove the following configuration:
Java Config Removal
// Remove this method from your WebMvcConfigurer implementation
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
XML Config Removal
<!-- Remove this from your Spring MVC configuration -->
<mvc:default-servlet-handler />
Custom Static Resource Filter
After disabling the default handler, serve static resources through a custom filter:
package com.example.app.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ResourceDeliveryFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String resourcePath = httpRequest.getServletPath();
String absolutePath = httpRequest.getServletContext().getRealPath(resourcePath);
try (InputStream inputStream = new java.io.FileInputStream(absolutePath);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
}
}
Register the filter programmatically:
public class AppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
FilterRegistration.Dynamic resourceFilter = servletContext.addFilter("resourceFilter", ResourceDeliveryFilter.class);
resourceFilter.addMappingForUrlPatterns(null, false, "/static/*");
}
}
Method 2: Controller-Level @ExceptionHandler
Define exception handling within individual controllers or a base class:
package com.example.app.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
public abstract class BaseWebController {
protected final Logger log = LoggerFactory.getLogger(getClass());
@ExceptionHandler(Exception.class)
public ModelAndView handleException(Exception exception) {
log.error("Exception caught in controller: {}", exception.getMessage(), exception);
return new ModelAndView("error");
}
@RequestMapping("*")
public String handleUnknownRequest() {
return "error";
}
}
Method 3: SimpleMappingExceptionResolver Bean
Configure exception-to-view mapping through a bean definition:
@Bean
public SimpleMappingExceptionResolver exceptionMappingResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.put("com.example.app.error.ResourceNotFoundException", "not-found");
mappings.put("com.example.app.error.AccessDeniedException", "access-denied");
resolver.setExceptionMappings(mappings);
Properties statusCodes = new Properties();
statusCodes.put("not-found", "404");
statusCodes.put("access-denied", "403");
resolver.setStatusCodes(statusCodes);
resolver.setDefaultErrorView("generic-error");
resolver.setDefaultStatusCode(500);
return resolver;
}
Combine with a catch-all request mapping for unhandled URLs:
@RequestMapping("*")
public String handleUnmappedRequest() {
return "error";
}
Method 4: HTTP Status Code Mapping with @ResponseStatus
Annotate custom exceptions to automatically translate them into HTTP status codes:
package com.example.app.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Throw this exception from your business logic:
public User retrieveUser(Long id) {
User user = userRepository.findById(id);
if (user == null) {
throw new ResourceNotFoundException("User with ID " + id + " does not exist");
}
return user;
}