Close

Spring MVC - Mixing web.xml and Spring exception handling

[Last Updated: Sep 18, 2026]

The Servlet specification allows error status codes and exceptions thrown by a web application to be mapped to pages or servlets (check out the related tutorial here). This mapping is specified in web.xml. Spring MVC operates on top of the Servlet specification, so it's still possible to mix the two approaches to error handling.

Spring overrides the mapping specified in web.xml. In other words, if an exception is not handled by Spring's HandlerExceptionResolver logic, the web.xml mapping is used. For example, if a NullPointerException is mapped to a page in web.xml and, at the same time, the same exception is also mapped by using a HandlerExceptionResolver, e.g. via @ExceptionHandler, then the Spring handler will be used instead of the web.xml mapping.

In cases where the request doesn't go through the DispatcherServlet and the requested resource isn't found, a 404 status code is returned by the server. web.xml allows such error codes to be mapped. Spring is only capable of mapping application-level exceptions, including Spring Framework exceptions.


In the following examples, we'll see how to mix web.xml and HandlerExceptionResolver mappings, and how Spring can override the web.xml mapping.



Exception mapping to a JSP page in web.xml

The Controller

@Controller
public class ExampleController {

    @RequestMapping("/test1")
    public String handleRequest1(Model model) {
        model.addAttribute("handler", "handleRequest1");
        throw new RuntimeException("test exception 1");
    }
    .............
}

src/main/webapp/WEB-INF/web.xml

<web-app ...>
 <error-page>
  <exception-type>java.lang.RuntimeException</exception-type>
  <location>/WEB-INF/views/exception-page.jsp</location>
 </error-page>
...
</web-app>

main/webapp/WEB-INF/views/exception-page.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<html>
<body>
<h3>This is a JSP  exception page</h3>
 <p>Spring Handler method: ${handler}</p>
 <p>
  Status:(jakarta.servlet.error.status_code)<br/>
       <%=request.getAttribute("jakarta.servlet.error.status_code") %>
 </p>
  <p>
   Status:(response.getStatus())<br/><%=response.getStatus() %>
  </p>
 <p>
  Reason:<br/><%=request.getAttribute("jakarta.servlet.error.message") %>
 </p>
 <p>
  Type:<br/><%=request.getAttribute("jakarta.servlet.error.exception_type") %>
 </p>
</body>
</html>

JavaConfig class

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

Running the example

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

mvn jetty:run

Output

$ curl -s "http://localhost:8080/example/test1"

<html>
<body>
<h3>This is a JSP exception page</h3>
<p>Spring Handler method: </p>
<p>
Status:(jakarta.servlet.error.status_code)<br/>
500
</p>
<p>
Status:(response.getStatus())<br/>500
</p>
<p>
Reason:<br/>jakarta.servlet.ServletException: Request processing failed: java.lang.RuntimeException: test exception 1
</p>
<p>
Type:<br/>class java.lang.RuntimeException
</p>
</body>
</html>

Exception mapping to another URI in web.xml

In this example, java.lang.ArithmeticException is mapped to another URI in web.xml, which in turn is mapped to a controller method in the Spring layer:

src/main/webapp/WEB-INF/web.xml

<web-app ...>
 <error-page>
  <exception-type>java.lang.ArithmeticException</exception-type>
  <location>/errorHandler</location>
 </error-page>
.....
 </web-app>

Controller

@Controller
public class ExampleController {
    .............
    @RequestMapping("/test3")
    @ResponseBody
    public String handleRequest3(Model model) {
        model.addAttribute("handler", "handleRequest3");
        int i = 20 / 0;
        return "testId: " + i;
    }
    .............
    @RequestMapping("/errorHandler")
    public String handleRequest5(Model model)  {
        model.addAttribute("handler", "errorHandler");
        return "exception-page";
    }
    .............
}

Output

$ curl -s "http://localhost:8080/example/test3"

<html>
<body>
<h3>This is a JSP exception page</h3>
<p>Spring Handler method: errorHandler</p>
<p>
Status:(jakarta.servlet.error.status_code)<br/>
500
</p>
<p>
Status:(response.getStatus())<br/>500
</p>
<p>
Reason:<br/>jakarta.servlet.ServletException: Request processing failed: java.lang.ArithmeticException: / by zero
</p>
<p>
Type:<br/>class java.lang.ArithmeticException
</p>
</body>
</html>


Mapping status code to a static page in web.xml

<web-app ...>
 ......
 <error-page>
  <error-code>404</error-code>
  <location>/static/custom-not-found-page.html</location>
 </error-page>
.......
</web-app>

src/main/webapp/static/custom-not-found-page.html

<html>
<body>
<p>Oops!! looks like there's nothing available at this location:<br/>
 <b><script>document.write(window.location.href)</script></b>
</p>
</body>
</html>

Registering resource handlers to serve static pages:

@EnableWebMvc
@ComponentScan
@Configuration
public class AppConfig implements WebMvcConfigurer {
    .............
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/");
    }
}

Output

$ curl -si "http://localhost:8080/example/xyz" | findstr /V /R "^Date: ^Content- ^Server: ^Cache-Control: ^Last-Modified: ^Accept-Ranges: ^Vary:"
HTTP/1.1 404 Not Found

<html>
<body>
<p>Oops!! looks like there's nothing available at this location:<br/>
<b><script>document.write(window.location.href)</script></b>
</p>
</body>
</html>


@ExceptionHandler mapping within controller

This example demonstrates that the functionality of Spring's default HandlerExceptionResolvers is not affected by web.xml settings.
We are going to use @ExceptionHandler (related resolver: ExceptionHandlerExceptionResolver)

Controller

@Controller
public class ExampleController {
    .............
    @RequestMapping("/test6")
    public String handleRequest6(Model model)  {
        model.addAttribute("handler", "handleRequest6");
        throw new SecurityException("test exception 6");
    }
    .............
    @ResponseStatus(HttpStatus.PRECONDITION_FAILED)
    @ExceptionHandler
    public String handleLocalException(SecurityException e,
                                       Model model,
                                       HttpServletRequest r) {
        model.addAttribute("handler",
                           "handleLocalException with @ExceptionHandler");
        return "exception-page";
    }
    .............
}

There's no related configuration in the JavaConfig class or mapping in web.xml

Output

$ curl -si "http://localhost:8080/example/test6" | findstr /V /R "^Date: ^Content- ^Server: ^Cache-Control: ^Last-Modified: ^Set-Cookie: ^Expires: ^Connection:"
HTTP/1.1 412 Precondition Failed


<html>
<body>
<h3>This is a JSP exception page</h3>
<p>Spring Handler method: handleLocalException with @ExceptionHandler</p>
<p>
Status:(jakarta.servlet.error.status_code)<br/>
200
</p>
<p>
Status:(response.getStatus())<br/>412
</p>
<p>
Reason:<br/>test exception 6
</p>
<p>
Type:<br/>class java.lang.SecurityException
</p>
</body>
</html>


Spring HandlerExceptionResolvers Override the web.xml mapping

This example shows that when the same mapping exists in both Spring and web.xml, Spring's mapping takes precedence.

web.xml

<web-app ...>
 ....
 <error-page>
  <exception-type>java.lang.IllegalAccessException</exception-type>
  <location>/WEB-INF/views/exception-page.jsp</location>
 </error-page>
 ....
</web-app>

Controller

@Controller
public class ExampleController {
    .............
    @RequestMapping("/test7")
    public String handleRequest7(Model model) throws Exception {
        model.addAttribute("handler", "handleRequest7");
        throw new IllegalAccessException("test exception 7");
    }
    .............
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler
    public String handleLocalException2(IllegalAccessException e,
                                        Model model) {
        model.addAttribute("handler",
                           "handleLocalException2 with @ExceptionHandler");
        return "exception-page";
    }
}

Output

$ curl -s "http://localhost:8080/example/test7"

<html>
<body>
<h3>This is a JSP exception page</h3>
<p>Spring Handler method: handleLocalException2 with @ExceptionHandler</p>
<p>
Status:(jakarta.servlet.error.status_code)<br/>
200
</p>
<p>
Status:(response.getStatus())<br/>401
</p>
<p>
Reason:<br/>test exception 7
</p>
<p>
Type:<br/>class java.lang.IllegalAccessException
</p>
</body>
</html>

In the above screenshot, the 'Spring Handler method:' value shows that the @ExceptionHandler method was used instead of the direct mapping to the JSP page in web.xml.


Default mapping to a page in web.xml

<web-app ...>
 ......
 <error-page>
  <location>/WEB-INF/views/default-error-page.jsp</location>
 </error-page>
</web-app>

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

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

<html>
<body>
<h3>This is default error page</h3>
 <p>Spring Handler method: ${handler}</p>
 <p>
  Status:(jakarta.servlet.error.status_code)<br/>
  <%=request.getAttribute("jakarta.servlet.error.status_code") %>
 </p>
  <p>
   Status:(response.getStatus())<br/><%=response.getStatus() %>
  </p>
 <p>
  Reason:<br/><%=request.getAttribute("jakarta.servlet.error.message") %>
 </p>
 <p>
  Type:<br/><%=request.getAttribute("jakarta.servlet.error.exception_type") %>
 </p>
</body>
</html>

Controller

java.lang.Exception is not mapped anywhere.

methods=handleRequest4

@Controller
public class ExampleController {
    .............
    @RequestMapping("/test4")
    public String handleRequest4(Model model) throws Exception {
        model.addAttribute("handler", "handleRequest4");
        throw new Exception("test exception");
    }
    .............
}

output

$ curl -s "http://localhost:8080/example/test4"


<html>
<body>
<h3>This is default error page</h3>
<p>Spring Handler method: </p>
<p>
Status:(jakarta.servlet.error.status_code)<br/>
500
</p>
<p>
Status:(response.getStatus())<br/>500
</p>
<p>
Reason:<br/>jakarta.servlet.ServletException: Request processing failed: java.lang.Exception: test exception
</p>
<p>
Type:<br/>class java.lang.Exception
</p>
</body>
</html>

Overriding default page in Spring

Just as we saw in the example above, where @ExceptionHandler + IllegalAccessException overrode the web.xml mapping, we can use other HandlerExceptionResolvers to override web.xml's default page.

For example, the following snippet shows SimpleMappingExceptionResolver setting a default page.

@Bean
  public HandlerExceptionResolver theResolver(){
     SimpleMappingExceptionResolver s = new SimpleMappingExceptionResolver();
     s.setDefaultErrorView("default-error-page2");
     return s;
  }


Spring internal exception handling

Spring's internal errors, such as MissingPathVariableException, are handled by DefaultHandlerExceptionResolver. A mapping like this, in web.xml, will be ignored unless we disable DefaultHandlerExceptionResolver:

<web-app ...>
 ....
 <error-page>
   <exception-type>
             org.springframework.web.bind.MissingPathVariableException
   </exception-type>
   <location>/WEB-INF/views/exception-page.jsp</location>
 </error-page>
 ......
</web-app>

DefaultHandlerExceptionResolver handles the internal exception and sends error information by calling HttpServletResponse.sendError(statusCode). This causes the servlet container (Tomcat, in our case) to show an error page (not an exception page with a full stack trace) along with the status code and error message only. Since this exception is treated as handled, the mapping above doesn't apply. We can, however, map the status code to a destination like this:

<web-app ...>
....
  <error-page>
   <error-code>500</error-code>
   <location>/WEB-INF/views/exception-page.jsp</location>
  </error-page>
......
</web-app>

Instead of mapping the status code to a destination, if a default mapping exists (as in the previous section), that mapping will be used.

Let's see an example

web.xml

<web-app ...>
 <error-page>
   <exception-type>
          org.springframework.web.bind.MissingPathVariableException
   </exception-type>
   <location>/WEB-INF/views/exception-page.jsp</location>
 </error-page>
 .....
 <error-page>
  <location>/WEB-INF/views/default-error-page.jsp</location>
 </error-page>
</web-app>

Controller

The following handler throws a MissingPathVariableException because the template variable 'id' doesn't match the @PathVariable value 'pid'.

@Controller
public class ExampleController {
    .............
    @RequestMapping("/test2/{id}")
    public String handleRequest2(@PathVariable("pid") String id,
                                 Model model) {
        model.addAttribute("handler", "handleRequest2");
        return "exception-page";
    }
    .............
}

Output

$ curl -s "http://localhost:8080/example/test2/5"


<html>
<body>
<h3>This is default error page</h3>
<p>Spring Handler method: </p>
<p>
Status:(jakarta.servlet.error.status_code)<br/>
500
</p>
<p>
Status:(response.getStatus())<br/>500
</p>
<p>
Reason:<br/>Required path variable 'pid' is not present.
</p>
<p>
Type:<br/>null
</p>
</body>
</html>

Note that in all the examples above, when an exception occurs in a Spring handler method, attributes populated in the Model object do not render any values in the error pages. As with the @ExceptionHandler case, where the original model attributes are not passed to the exception handler method, a handler method's original Model object is not intended for use in the error page. The HandlerExceptionResolver#resolveException method does not have access to the original Model information to pass on to the error page.


Example Project

Dependencies and Technologies Used:

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

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 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.

  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • JDK 25
  • Maven 3.9.11

Spring MVC - Mixing web.xml and Spring exception handling Select All Download
  • servlet-spring-mixed-exception-handling
    • src
      • main
        • java
          • com
            • logicbig
              • example
        • webapp
          • WEB-INF
            • views
            • web.xml
            • static

    See Also

    Join