skip to content

What exceptions can the HandlerExceptionResolver chain NOT handle, and what are the key edge cases (filters, committed responses, view rendering, async)?

level: principalimportance: nice to knowfreq 18%

answer

  1. chain = mapping + interceptors + handler execution only
  2. filters run outside DispatcherServlet → bypass resolvers
  3. committed response → can't change status
  4. async: ASYNC dispatch re-runs chain; timeout → 503
  5. final fallback = container ERROR dispatch to /error (separate mechanism)

basics

~20 s

The chain only handles exceptions thrown inside DispatcherServlet's dispatch (handler mapping, interceptors, handler execution). It cannot handle exceptions from Servlet Filters (they run before DispatcherServlet), from view rendering after the response is committed, or after the response is already flushed. Those fall to the container's error page.

solid answer

~50 s

HandlerExceptionResolvers are invoked by DispatcherServlet.processHandlerException, so they only see exceptions thrown during the dispatch: handler mapping, HandlerInterceptor preHandle, and handler (controller) execution. They do NOT see: exceptions thrown in Servlet Filters (those wrap and run outside DispatcherServlet — handled by the container/Boot ErrorPageFilter or /error), exceptions during view rendering once the response is already committed (status can't change), or anything after the response is flushed. For async requests, exceptions from the async dispatch are re-run through the resolver chain on the ASYNC dispatch, but a timeout produces AsyncRequestTimeoutException handled normally. Also, a resolver that itself throws breaks the chain. Unresolved exceptions are rethrown, propagate through filters, and in Spring Boot trigger an ERROR dispatch to /error (BasicErrorController) — the true last line of defense. Understanding this boundary explains why a security filter's 401 or a mid-render failure never reaches your @ExceptionHandler.

code

java · 24 lines
java
// This exception is thrown in a FILTER -> resolvers never see it,
// it hits the container / Boot ErrorPageFilter -> /error.
class GuardFilter extends OncePerRequestFilter {
    @Override protected void doFilterInternal(HttpServletRequest req,
            HttpServletResponse res, FilterChain chain) throws ServletException {
        if (blocked(req)) {
            // An @ExceptionHandler for this type will NOT catch it:
            throw new AccessBlockedException();
        }
        chain.doFilter(req, res); // DispatcherServlet lives inside here
    }
}

// To give container-level failures the SAME body as your @ExceptionHandler,
// customize the /error endpoint instead:
@Component
class ProblemErrorAttributes extends DefaultErrorAttributes {
    @Override public Map<String, Object> getErrorAttributes(
            WebRequest request, ErrorAttributeOptions options) {
        Map<String, Object> attrs = super.getErrorAttributes(request, options);
        attrs.put("contract", "v1"); // unify shape across MVC + filter errors
        return attrs;
    }
}

go deeper

for a junior

Know that not every exception reaches @ExceptionHandler; some go to the container error page.

for a middle

Identify that filter exceptions and post-commit failures bypass the resolver chain.

for a senior

Explain the /error fallback as a separate mechanism and the async re-dispatch behavior.

for a principal

Architect a unified error contract spanning MVC resolvers, security filters, and the container /error page, and reason about the commit boundary.

## The scope of the chain `DispatcherServlet.doDispatch()` structure (simplified): ``` try { mappedHandler = getHandler(request); // mapping ... interceptor.preHandle ... ha.handle(request, response, handler); // controller executes ... interceptor.postHandle ... } catch (Exception ex) { dispatchException = ex; // captured } processDispatchResult(...); // -> processHandlerException(...) runs the resolver chain ``` So the chain covers: **handler mapping, preHandle interceptors, controller/handler execution, and return-value handling**. That is the whole span the resolvers can rescue. ## What the chain CANNOT handle ### 1. Exceptions in Servlet Filters Filters run in the container's filter chain, **wrapping** DispatcherServlet. An exception thrown in a `Filter` (e.g., a Spring Security authentication filter, or a custom logging filter) happens *before or after* DispatcherServlet's try/catch and is **never** offered to the resolvers. It goes straight to the container. This is why Spring Security's 401/403 responses are produced by its own `AuthenticationEntryPoint`/`AccessDeniedHandler`, not by `@ExceptionHandler` (unless you bridge them). Spring Boot's `ErrorPageFilter` can catch some filter exceptions and forward to `/error`. ### 2. Exceptions during view rendering after commit `processDispatchResult` renders the view *after* the resolver chain has run. If rendering throws (e.g., a template error) and the response is **already committed** (headers/first bytes flushed), no resolver can change the status — you get a broken/partial response. If not yet committed, DispatcherServlet may attempt to handle it, but this is fragile. ### 3. After the response is committed Once `response.isCommitted()` is true, resolvers can log but cannot alter status/headers. `sendError`/`setStatus` become no-ops or throw `IllegalStateException`. ### 4. Errors before dispatch Request parsing failures at the container level, or issues before DispatcherServlet is even reached, are outside the chain. ## Async edge cases - For `Callable`/`DeferredResult`/`WebAsyncTask`, when the async result completes an **ASYNC dispatch** re-enters DispatcherServlet, and exceptions raised producing the result **are** run through the resolver chain again. - A timeout raises `AsyncRequestTimeoutException` → DefaultHandlerExceptionResolver maps it to **503**. ## A resolver that throws Resolvers should return `null` when they don't handle an exception. If a resolver **throws**, that new exception escapes `processHandlerException` and propagates — potentially masking the original. Defensive resolvers wrap their own logic. ## The final fallback: /error When all resolvers return null, the original exception is rethrown out of DispatcherServlet, back through the filters, to the container. Spring Boot registers an error page at `/error`; the container performs an internal **ERROR dispatch**, and `BasicErrorController` renders JSON (or the Whitelabel HTML page) using `ErrorAttributes`. This is a *different* mechanism from the resolver chain — it is the container's error-page facility, not a HandlerExceptionResolver. You customize it via `ErrorController`/`ErrorAttributes`, not `@ExceptionHandler`. ## Why this matters (principal-level) - Cross-cutting concerns that must handle *all* failures (including filter-level) belong at the filter/error-page layer, not `@ControllerAdvice`. - A consistent API error contract needs both an `@ExceptionHandler`/`ResponseEntityExceptionHandler` (for in-dispatch exceptions) *and* a customized `/error` (for filter/async/servlet-level failures) to avoid two divergent error shapes. - Security errors are intentionally outside the MVC chain; unifying them requires deliberate bridging. ## Gotchas summary - Filter exceptions bypass resolvers. - Committed response = resolvers powerless. - View-render failure after commit = broken response. - /error is a separate, last-resort mechanism, not a resolver.

  • Why doesn't a global @ControllerAdvice @ExceptionHandler catch Spring Security's 403?
    Spring Security's exception translation happens in its filter chain, outside DispatcherServlet, so the resolver chain (including ExceptionHandlerExceptionResolver) never sees it. Security uses its own AccessDeniedHandler/AuthenticationEntryPoint. To unify, bridge them or customize /error.
  • How would you guarantee a single, consistent error body shape across controller exceptions AND filter/servlet-level failures?
    Combine two layers: a ResponseEntityExceptionHandler/@ControllerAdvice for in-dispatch exceptions, plus a customized /error (custom ErrorAttributes or ErrorController) producing the same shape for anything that escapes to the container. Optionally reuse the same DTO/ProblemDetail builder in both.
  • What happens if view rendering throws after the response has been committed?
    Resolvers cannot change the already-sent status/headers, so you get a truncated or malformed response. The failure is logged but the client sees a broken payload — a reason to keep rendering side-effect-free and detect errors before writing.

saying these in an interview costs you the question

  • Believing @ExceptionHandler / @ControllerAdvice catches exceptions thrown in Servlet Filters
  • Thinking /error (BasicErrorController) is itself a HandlerExceptionResolver
  • Assuming a resolver can fix the status after the response is committed

context