skip to content

When a Spring MVC controller throws an exception, how does the framework turn it into an HTTP response?

level: juniorimportance: must knowfreq 45%

answer

  1. DispatcherServlet.processHandlerException
  2. loop resolvers until non-null ModelAndView
  3. empty ModelAndView = handled, no view
  4. null everywhere → rethrow → /error
  5. 3 defaults in fixed order

basics

~20 s

The DispatcherServlet catches the exception and offers it to a list of HandlerExceptionResolvers. The first one that can handle it writes the response (status and body). If none handle it, the exception propagates to the servlet container.

solid answer

~30 s

When a handler (or interceptor) throws, DispatcherServlet does not let the exception escape immediately. It calls processHandlerException, which loops over its ordered list of HandlerExceptionResolver beans, invoking resolveException(request, response, handler, ex) on each. The first resolver that returns a non-null ModelAndView wins and the loop stops; an empty ModelAndView means 'handled, response already written.' If every resolver returns null, the original exception is rethrown and bubbles up to the servlet container, which in Spring Boot triggers an ERROR dispatch to /error (BasicErrorController). By default Spring registers three resolvers: ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, and DefaultHandlerExceptionResolver, tried in that order.

code

java · 23 lines
java
// The strategy DispatcherServlet iterates over:
public interface HandlerExceptionResolver {
    // null  -> not my job, try next resolver
    // ModelAndView (possibly empty) -> handled
    ModelAndView resolveException(HttpServletRequest request,
                                  HttpServletResponse response,
                                  Object handler,
                                  Exception ex);
}

// A minimal custom resolver:
public class TeapotResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest req,
                                         HttpServletResponse res,
                                         Object handler, Exception ex) {
        if (ex instanceof CoffeeRequestedException) {
            res.setStatus(418); // I'm a teapot
            return new ModelAndView(); // empty = response handled
        }
        return null; // let the next resolver try
    }
}

go deeper

for a junior

Know that DispatcherServlet delegates to a list of HandlerExceptionResolvers and the first that handles it wins.

for a middle

Name the three default resolvers and the null-vs-ModelAndView contract.

for a senior

Explain the fall-through to /error and the boundary (filters/committed responses are outside the chain).

for a principal

Discuss how this SPI unifies every error mechanism and how to extend it without breaking defaults.

## The problem A `@Controller` method can throw any exception. Something has to convert that thrown `Throwable` into a proper HTTP response (a status code, headers, and optionally a body). In Spring MVC that job belongs to the **HandlerExceptionResolver** strategy. ## HandlerExceptionResolver — the SPI It is a single-method interface: ```java public interface HandlerExceptionResolver { ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); } ``` - Return **null** = "I don't handle this exception, try the next resolver." - Return a **non-null ModelAndView** = "handled." An *empty* ModelAndView (`mav.isEmpty()` / `mav.clearEmptyRequest`) signals the response has already been fully written (e.g., status set directly), so no view needs rendering. ## Where it plugs in — DispatcherServlet `DispatcherServlet.doDispatch()` wraps handler mapping and handler execution in a try/catch. A caught `Exception` is stored, and `processDispatchResult(...)` calls `processHandlerException(request, response, handler, ex)`. That method iterates `this.handlerExceptionResolvers` (a `List<HandlerExceptionResolver>`), calling `resolveException` on each **in order** until one returns non-null. That first non-null result short-circuits the loop. ## The default chain When you use `@EnableWebMvc` or Spring Boot's autoconfiguration, `WebMvcConfigurationSupport` registers three resolvers, tried in this order: 1. **ExceptionHandlerExceptionResolver** — dispatches to your `@ExceptionHandler` methods (in controllers or `@ControllerAdvice`). 2. **ResponseStatusExceptionResolver** — handles exceptions carrying `@ResponseStatus` and `ResponseStatusException`. 3. **DefaultHandlerExceptionResolver** — maps Spring MVC's own built-in exceptions (e.g., 405, 415, 400) to status codes. ## What if nobody handles it? If all three return null, `processHandlerException` **rethrows** the exception. It leaves DispatcherServlet, goes to any Filters, then to the servlet container. In Spring Boot the container performs an internal ERROR dispatch to `/error`, served by `BasicErrorController`, producing the default JSON/Whitelabel error page. ## Key gotchas - The chain only sees exceptions thrown **inside** DispatcherServlet's dispatch (handler mapping, interceptors, handler execution). Exceptions from **Filters** run *before* DispatcherServlet and are never seen by these resolvers. - Once the response is **committed** (bytes flushed), a resolver can no longer change status — you may get a truncated/garbled response. ## When to care Every Spring MVC error-handling feature — `@ExceptionHandler`, `@ResponseStatus`, `ResponseStatusException`, the automatic 405/415 responses — is just a resolver in this chain. Understanding the chain explains *why* one mechanism wins over another.

  • What is the difference between a resolver returning null and returning an empty ModelAndView?
    null means 'I did not handle this — try the next resolver.' An empty ModelAndView means 'handled; I already wrote the response (e.g., set the status), so DispatcherServlet should not render any view.' A populated ModelAndView means 'handled — render this view/model.'
  • If no resolver handles the exception, where does it go?
    It is rethrown out of DispatcherServlet, passes back through the filter chain, and reaches the servlet container, which triggers an ERROR dispatch. In Spring Boot that lands on BasicErrorController at /error.

saying these in an interview costs you the question

  • Thinking the exception is caught by a try/catch in the controller framework and swallowed silently
  • Believing @ExceptionHandler is a special language feature rather than one resolver in a chain
  • Assuming exceptions thrown in a Servlet Filter are handled by these resolvers

context