Spring MVC provides several mechanisms for error handling, all built on the HandlerExceptionResolver interface.
Compared with Servlet-based error handling, where an 'HTTP error code' or a 'Java exception' is mapped to a servlet or a page, the HandlerExceptionResolver interface deals only with 'Java exceptions' raised during controller execution. Because exception scenarios can be handled programmatically, this approach is more flexible than the web.xml mapping approach.
The HandlerExceptionResolver interface has only one method, as shown in the following snippet:
Definition of HandlerExceptionResolverVersion: 7.0.6 package org.springframework.web.servlet;
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, 1
HttpServletResponse response,
@Nullable Object handler,
Exception ex);
}
The 'handler' parameter contains information about the controller's handler method in which the exception occurred. In Spring MVC, this parameter is normally an instance of HandlerMethod. The resolveException() method is invoked even when the exception is thrown outside the target handler method itself — for example, in a handler interceptor — as long as it occurs in the course of handling that request.
Example
Creating and registering a HandlerExceptionResolver implementation as a bean.
package com.logicbig.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@ComponentScan
@Configuration
@EnableWebMvc
public class Config implements WebMvcConfigurer {
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
HandlerExceptionResolver handlerExceptionResolver = new HandlerExceptionResolver() {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
ModelAndView model = new ModelAndView("error-page");
model.addObject("exceptionType", ex);
model.addObject("handlerMethod", handler);
return model;
}
};
exceptionResolvers.add(handlerExceptionResolver);
}
//registering an interceptor
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (request.getParameter("testParam") != null) {
throw new Exception("exception from interceptor");
}
return true;
}
});
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/pages/", ".jsp");
}
}
The Controller
package com.logicbig.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class ExampleController {
@RequestMapping("/")
public void handleRequest () {
//just for testing
throw new RuntimeException();
}
@RequestMapping("/test")
@ResponseBody
public String testHandler () {
return "test body";
}
}
src/main/webapp/WEB-INF/pages/error-page.jsp
<%@ page language="java"
contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<body>
<h3>This is example exception page</h3>
<p>Exception: <b>${exceptionType}</b></p>
<p>Handler method: <b> ${handlerMethod} </b></p>
</body>
</html>
Note: JSP is still supported as a Spring MVC view technology via InternalResourceViewResolver, but it has become a legacy choice for new applications; most current projects favor a template engine such as Thymeleaf. It is kept here only to illustrate the resolver's interaction with the view layer.
Running the application
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
output
Exception thrown in our HandlerInterceptor:
Pre Spring 6.1, HandlerExceptionResolver.resolveException() is not invoked for HTTP status errors, e.g., a 404.
Note: since Spring Framework 6.1, the throwExceptionIfNoHandlerFound property of DispatcherServlet defaults to true (the setter is now deprecated, since there's no reason to turn it off). This means an unmapped URL no longer results in a quiet 404 response — it raises a NoHandlerFoundException that is routed through the normal HandlerExceptionResolver chain, just like any other exception. Whether it reaches a custom, catch-all resolver like the one above depends on whether Spring's built-in DefaultHandlerExceptionResolver is still present in that chain.
Spring 6.1+ an unmapped URL no longer results in a quiet 404 response:
Integration Test
package com.logicbig.example;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.NoHandlerFoundException;
import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = Config.class)
public class ExceptionHandlingTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvcTester;
@BeforeEach
public void setup() {
mockMvcTester = MockMvcTester.from(wac);
}
@Test
public void controllerExceptionIsResolvedToErrorPage() {
assertThat(mockMvcTester.get().uri("/").exchange())
.hasStatusOk()
.hasForwardedUrl("/WEB-INF/pages/error-page.jsp")
.model()
.containsKey("handlerMethod")
.extractingByKey("exceptionType",
as(InstanceOfAssertFactories.type(
RuntimeException.class)))
.isNotNull();
}
@Test
public void interceptorExceptionIsResolvedToErrorPage() {
assertThat(mockMvcTester.get().uri("/").param("testParam", "1")
.exchange())
.hasStatusOk()
.hasForwardedUrl("/WEB-INF/pages/error-page.jsp")
.model()
.containsKey("handlerMethod")
.extractingByKey("exceptionType",
as(InstanceOfAssertFactories.type(Exception.class)))
.extracting(Exception::getMessage)
.isEqualTo("exception from interceptor");
}
@Test
public void unmappedUrlErrorPage() {
assertThat(mockMvcTester.get().uri("/no such url")
.exchange())
.hasStatusOk()
.hasForwardedUrl("/WEB-INF/pages/error-page.jsp")
.model()
.containsEntry("handlerMethod", null)
.extractingByKey("exceptionType",
as(InstanceOfAssertFactories.type(
NoHandlerFoundException.class)))
.isNotNull();
}
}
mvn clean test -Dtest=ExceptionHandlingTest Output$ mvn clean test -Dtest=ExceptionHandlingTest [INFO] Scanning for projects... [INFO] [INFO] ---------< com.logicbig.example:spring-mvc-exception-handling >--------- [INFO] Building spring-mvc-exception-handling 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ spring-mvc-exception-handling --- [INFO] Deleting D:\example-projects\spring-mvc\spring-mvc-exception-handling\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ spring-mvc-exception-handling --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-mvc-exception-handling\src\main\resources [INFO] [INFO] --- compiler:3.16.0:compile (default-compile) @ spring-mvc-exception-handling --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 3 source files with javac [debug target 25] to target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-mvc-exception-handling --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-mvc-exception-handling\src\test\resources [INFO] [INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ spring-mvc-exception-handling --- [INFO] Recompiling the module because of changed dependency. [INFO] Compiling 2 source files with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ spring-mvc-exception-handling --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.ExceptionHandlingTest [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.752 s -- in com.logicbig.example.ExceptionHandlingTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 8.580 s [INFO] Finished at: 2026-09-12T07:18:41-05:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 4 ms INFO: Completed initialization in 1 ms INFO: Completed initialization in 1 ms WARNING: No mapping for GET /no%20such%20url
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.6 (Spring TestContext Framework)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|