Close

Spring MVC - Replacing Spring default HandlerExceptionResolvers

[Last Updated: Sep 16, 2026]

In this example, we will replace Spring's default HandlerExceptionResolvers with a custom one.

Prerequisite

Ordering and customization of default HandlerExceptionResolvers.

Example

Steps

  1. In WebApplicationInitializer, we will set the detectAllHandlerExceptionResolvers property of DispatcherServlet to false. Doing so will register just one single bean of type HandlerExceptionResolver and will replace the default ones.
  2. Create a custom HandlerExceptionResolverComposite which will add ExceptionHandlerExceptionResolver and SimpleMappingExceptionResolver to its list. The desired functionality, in an exception scenario, is: if an @ExceptionHandler method is defined for an exception, then that will process the exception; otherwise a default page will be used (set by SimpleMappingExceptionResolver).
  3. In an @EnableWebMvc class, register the custom HandlerExceptionResolver with the name DispatcherServlet#HANDLER_EXCEPTION_RESOLVER_BEAN_NAME.
  4. Create default-error.jsp
  5. Create a controller class.

WebApplicationInitializer implementation

package com.logicbig.example;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class AppInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{AppConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected DispatcherServlet createDispatcherServlet(WebApplicationContext wac) {
        DispatcherServlet ds = new DispatcherServlet(wac);
        ds.setDetectAllHandlerExceptionResolvers(false);
        return ds;
    }
}

Creating a custom HandlerExceptionResolverComposite and registering it as a bean

package com.logicbig.example;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import java.util.Arrays;

@EnableWebMvc
@ComponentScan
@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Bean(name = DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME)
    HandlerExceptionResolver customExceptionResolver(ApplicationContext ac) {

        ExceptionHandlerExceptionResolver e = new ExceptionHandlerExceptionResolver();
        e.setApplicationContext(ac);
        e.afterPropertiesSet();

        SimpleMappingExceptionResolver s = new SimpleMappingExceptionResolver();
        s.setDefaultErrorView("default-error");
        s.setDefaultStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());

        HandlerExceptionResolverComposite c = new HandlerExceptionResolverComposite();
        c.setExceptionResolvers(Arrays.asList(e, s));
        return c;
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/", ".jsp");
    }
}

src/main/webapp/WEB-INF/views/default-error.jsp

<%@ page language="java"
         contentType="text/html; charset=ISO-8859-1"
         pageEncoding="ISO-8859-1" %>

<html>
<body>
<h3>This is default exception page</h3>
<p>Message: <b>${exception.message}</b></p>
<p>Exception: <b>${exception['class'].name}</b></p>
</body>
</html>

The Controller

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class ExampleController {

    @RequestMapping("/test")
    public String handleRequest() throws Exception {
        throw new IllegalAccessException();
    }

    /**
     * This will throw MissingPathVariableException with response code 500
     * because id != testId
     */
    @RequestMapping("/test/{id}")
    @ResponseBody
    public String handleRequest2(@PathVariable("testId") String id) throws Exception {
        return "testId: " + id;
    }

    @RequestMapping("/test2")
    public void handleRequest3() throws Exception {
        throw new Exception("test exception 2");
    }

    @ExceptionHandler
    @ResponseBody
    public String handleException(IllegalAccessException b) {
        return "from @ExceptionHandler: " + b;
    }
}

Running the example

To try examples, run embedded Jetty (configured in pom.xml of example project below):

mvn jetty:run

Output

URI: /test/{id}

This should throw a MissingPathVariableException, which will be mapped to default-error.jsp.


URI: /test

This should invoke the handleException(..) method of our controller. This method is annotated with @ExceptionHandler and has a parameter of the matching exception type.

This shows that ExceptionHandlerExceptionResolver has processed the exception as expected.


URI: /test2

The exception thrown in the corresponding controller method is not handled by any @ExceptionHandler, so default-error.jsp will be processed:


Other URIs

On Spring Framework versions before 6.1, /others returns a plain 404 Not Found, since DispatcherServlet#throwExceptionIfNoHandlerFound defaulted to false and an unmatched request was simply handled by the servlet container's own 404 handling, without ever going through a HandlerExceptionResolver. On the current version shown on this page, that flag is always effectively true (the setter was removed as of 7+), so a NoHandlerFoundException is now always raised and flows into the HandlerExceptionResolverComposite above, where it falls through to the SimpleMappingExceptionResolver's default error view — resulting in a 500 response instead.

Integration Test

package com.logicbig.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type;

@SpringJUnitWebConfig(AppConfig.class)
public class ExampleControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvcTester mockMvcTester;

    @BeforeEach
    public void setup() {
        mockMvcTester = MockMvcTester.from(webApplicationContext);
    }

    @Test
    public void test_endpoint() {
        assertThat(mockMvcTester.get().uri("/test"))
                .hasStatusOk()
                .bodyText().isEqualTo(
                        "from @ExceptionHandler: java.lang.IllegalAccessException");
    }

    @Test
    public void testResolvesToDefaultErrorView() {
        assertThat(mockMvcTester.get().uri("/test/4"))
                .hasStatus5xxServerError()
                .hasViewName("default-error")
                .model().extractingByKey("exception")
                .asInstanceOf(type(MissingPathVariableException.class))
                .extracting(Throwable::getMessage).asString()
                .contains("testId");
    }

    @Test
    public void test2_endpoint() {
        assertThat(mockMvcTester.get().uri("/test2"))
                .hasStatus5xxServerError()
                .hasViewName("default-error")
                .model().extractingByKey("exception")
                .asInstanceOf(type(Exception.class))
                .extracting(Throwable::getMessage).asString()
                .isEqualTo("test exception 2");
    }

    @Test
    public void others_endpoint() {
        assertThat(mockMvcTester.get().uri("/others"))
                .hasStatus5xxServerError();
    }
}

Output

$ mvn clean test -Dtest="ExampleControllerTest"
[INFO] Scanning for projects...
[INFO]
[INFO] -----< com.logicbig.example:replacing-default-exception-resolvers >-----
[INFO] Building replacing-default-exception-resolvers 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ replacing-default-exception-resolvers ---
[INFO] Deleting D:\example-projects\spring-mvc\replacing-default-exception-resolvers\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ replacing-default-exception-resolvers ---
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\replacing-default-exception-resolvers\src\main\resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ replacing-default-exception-resolvers ---
[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) @ replacing-default-exception-resolvers ---
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\replacing-default-exception-resolvers\src\test\resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ replacing-default-exception-resolvers ---
[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) @ replacing-default-exception-resolvers ---
[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.ExampleControllerTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.195 s -- in com.logicbig.example.ExampleControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.435 s
[INFO] Finished at: 2026-09-16T02:36:22-05:00
[INFO] ------------------------------------------------------------------------
INFO: Completed initialization in 2 ms
INFO: Completed initialization in 1 ms
WARNING: No mapping for GET /others
INFO: Completed initialization in 3 ms
INFO: Completed initialization in 1 ms

Another way to replace default resolvers

Instead of turning off detectAllHandlerExceptionResolvers, there's another way to swap out the defaults: just define your own HandlerExceptionResolver bean and name it exactly "handlerExceptionResolver". That name isn't arbitrary — it's the same bean name Spring internally uses for its own default set of exception resolvers (registered by WebMvcConfigurationSupport). So when your bean uses that name too, it takes the place of the default one instead of being added alongside it.

The advantage of this approach: since detectAllHandlerExceptionResolvers is left at its default (true), Spring is still scanning for every HandlerExceptionResolver bean in the context — not just one specially-named one. So you're free to define several separate resolver beans, and they'll all be picked up, while still having fully replaced Spring's defaults.

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 4.2.0.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 4.2.0.RELEASE
    • 4.2.1.RELEASE
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 6.2.18
    • 6.2.19
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    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

Spring MVC - Replacing Default HandlerExceptionResolvers Select All Download
  • replacing-default-exception-resolvers
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • AppConfig.java
          • webapp
            • WEB-INF
              • views
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join