Mastering Spring MVC Exception Handling: From DispatcherServlet to Custom Resolvers
This article explains how Spring MVC handles exceptions, detailing the role of DispatcherServlet, the chain of HandlerExceptionResolver implementations, the controller invocation flow, and the internal mechanisms that resolve errors, including fallback to the default /error endpoint.
Overview
If an exception occurs during request mapping or is thrown from a controller, DispatcherServlet delegates to a chain of HandlerExceptionResolver beans to resolve the exception and produce an alternative response, usually an error view.
Available HandlerExceptionResolver implementations
SimpleMappingExceptionResolver : maps exception class names to error view names, used to render error pages in web applications.
DefaultHandlerExceptionResolver : resolves exceptions raised by Spring MVC and maps them to HTTP status codes.
ResponseStatusExceptionResolver : resolves exceptions annotated with @ResponseStatus to the corresponding HTTP status code.
ExceptionHandlerExceptionResolver : resolves exceptions by invoking methods annotated with @ExceptionHandler in @Controller or @ControllerAdvice classes.
Multiple HandlerExceptionResolver beans can be declared and ordered; a higher order value means lower priority (processed later).
HandlerExceptionResolver can return:
A ModelAndView pointing to an error view.
An empty ModelAndView if the exception was handled within the resolver.
null, allowing the next resolver in the chain to try; if still unresolved, the exception bubbles up to the servlet container.
Controller invocation flow
Spring MVC processes a request through DispatcherServlet with the following core steps:
HandlerMapping determines the target Handler object (e.g., a HandlerMethod for a @Controller).
The HandlerAdapter for that Handler is selected.
For a typical @Controller, the HandlerAdapter is RequestMappingHandlerAdapter.
<code>public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(...) throws Exception {
HandlerExecutionChain mappedHandler = null;
try {
Exception dispatchException = null;
// Determine Handler based on request (iterate all HandlerMapping)
mappedHandler = getHandler(processedRequest);
// Determine HandlerAdapter for the Handler
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Execute the target method
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
}</code> <code>public class RequestMappingHandlerAdapter {
protected ModelAndView handleInternal(...) throws Exception {
ModelAndView mav;
mav = invokeHandlerMethod(request, response, handlerMethod);
}
protected ModelAndView invokeHandlerMethod(...) throws Exception {
// ...
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
// configure invocableMethod
invocableMethod.invokeAndHandle(webRequest, mavContainer);
return getModelAndView(mavContainer, modelFactory, webRequest);
}
}</code> <code>public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
public void invokeAndHandle(...) throws Exception {
// Parse request parameters and invoke target method
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// Handle return value
try {
this.returnValueHandlers.handleReturnValue(returnValue,
getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
throw ex;
}
}
}</code>If an exception occurs during this process, it propagates to DispatcherServlet, where it is handled by the exception resolution mechanism.
Exception resolution analysis
After an exception, DispatcherServlet#processDispatchResult invokes the configured HandlerExceptionResolver chain.
<code>public class DispatcherServlet extends FrameworkServlet {
private List<HandlerExceptionResolver> handlerExceptionResolvers;
private void processDispatchResult(...) {
if (exception != null) {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
}
}
protected ModelAndView processHandlerException(...) throws Exception {
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
}
}
// ...
}
}</code>ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver, which in turn extends AbstractHandlerExceptionResolver. The resolution flow:
<code>public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered {
public ModelAndView resolveException(...) {
ModelAndView result = doResolveException(request, response, handler, ex);
}
}</code> <code>public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver {
protected final ModelAndView doResolveException(...) {
HandlerMethod handlerMethod = (handler instanceof HandlerMethod hm ? hm : null);
return doResolveHandlerMethodException(request, response, handlerMethod, ex);
}
}</code> <code>public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
protected ModelAndView doResolveHandlerMethodException(...) {
// Find @ExceptionHandler method in the current controller
// If not found, search global @ControllerAdvice handlers
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
// Invoke the exception handling method
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
}
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(...) {
// Resolve method annotated with @ExceptionHandler matching the exception
// Search in controller and then in @ControllerAdvice beans
}
}</code>If none of the resolvers can handle the exception, Spring falls back to the default error endpoint /error , provided by BasicErrorController.
<code>public class ErrorMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes,
this.serverProperties.getError(), errorViewResolvers.orderedStream().toList());
}
}</code> <code>@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController { }</code>The /error endpoint is automatically registered with the embedded servlet container (e.g., Tomcat) at application startup.
End of analysis.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.