skip to content

Explain how Spring resolves and invokes local @ExceptionHandler methods internally, and what such handlers cannot catch.

level: principalimportance: nice to knowfreq 26%

answer

  1. DispatcherServlet.processHandlerException -> resolver chain
  2. ExceptionHandlerExceptionResolver + ExceptionHandlerMethodResolver (cached)
  3. ServletInvocableHandlerMethod invokes it; ModelAndView out
  4. can't catch: filters, self-thrown, post-commit, other controllers
  5. no match -> next resolver -> container /error

basics

~20 s

DispatcherServlet delegates a thrown exception to its HandlerExceptionResolvers. ExceptionHandlerExceptionResolver finds the controller's @ExceptionHandler methods via ExceptionHandlerMethodResolver and invokes the best match. It can't catch exceptions from Servlet Filters, after the response is committed, or thrown by the handler itself.

solid answer

~40 s

When a controller method throws, DispatcherServlet.processDispatchResult loops its ordered HandlerExceptionResolver chain. ExceptionHandlerExceptionResolver runs first: it takes the failing HandlerMethod's controller, gets (or builds and caches) an ExceptionHandlerMethodResolver for that class, and resolves the best @ExceptionHandler by hierarchy depth, falling back to the exception's cause. It then invokes the handler through a ServletInvocableHandlerMethod, applying argument resolvers and return-value handlers, and produces a ModelAndView (possibly empty for a written body). If it returns null, the next resolver (ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver) gets a turn. Limits: it only covers exceptions from within DispatcherServlet's handler execution — not Servlet Filter exceptions, not async errors after commit, not exceptions thrown by the exception handler itself (those propagate), and not errors once the response is already committed. Unhandled exceptions fall through to the container /error dispatch.

code

java · 19 lines
java
// Conceptual sketch of the DispatcherServlet flow (not user code):
// try {
//   mv = ha.handle(request, response, handler);       // controller throws
// } catch (Exception ex) {
//   mv = processHandlerException(request, response, handler, ex);
// }
//
// processHandlerException:
//   for (HandlerExceptionResolver r : handlerExceptionResolvers) {
//       ModelAndView mv = r.resolveException(request, response, handler, ex);
//       if (mv != null) return mv;   // ExceptionHandlerExceptionResolver first
//   }
//   throw ex; // -> container error dispatch to /error

// A self-throwing handler is NOT re-handled:
@ExceptionHandler(FooException.class)
String handle(FooException ex) {
    throw new IllegalStateException("boom"); // propagates, no second @ExceptionHandler runs
}

go deeper

for a junior

Just know DispatcherServlet routes the exception to a resolver that finds the handler.

for a middle

Know the resolver name and that unmatched exceptions fall through to /error.

for a senior

Describe the resolver chain order, per-class caching, and the main uncatchable cases.

for a principal

Reason about resolver ordering interactions, cause-traversal surprises, and where a complete error contract needs Servlet-level or advice-level fallbacks beyond local handlers.

## The dispatch path 1. `DispatcherServlet.doDispatch` invokes the mapped handler (your controller method) via `HandlerAdapter`. 2. If it throws, the exception is caught and passed to `processDispatchResult`, which calls `processHandlerException`. 3. `processHandlerException` iterates the **ordered list of `HandlerExceptionResolver` beans**. The default chain is: **`ExceptionHandlerExceptionResolver`** → `ResponseStatusExceptionResolver` → `DefaultHandlerExceptionResolver`. 4. The first resolver that returns a non-null `ModelAndView` wins; others are skipped. ## Inside `ExceptionHandlerExceptionResolver` - It receives the `HandlerMethod` that failed, so it knows the **controller instance/class**. - It obtains an **`ExceptionHandlerMethodResolver`** for that controller class (cached per class). That resolver scanned the class at startup for `@ExceptionHandler` methods and built a `type → method` map. - **Resolution:** `resolveMethod` finds the best match using `ExceptionDepthComparator` (smallest inheritance depth), and if there's no direct match, walks the exception's **cause chain**. - **Invocation:** the chosen method is wrapped as a **`ServletInvocableHandlerMethod`**; Spring resolves its arguments with `HandlerMethodArgumentResolver`s and processes the return value with `HandlerMethodReturnValueHandler`s (the same machinery as normal controller methods, minus body-binding resolvers). - It also considers **controller-local first, then `@ControllerAdvice`** — the same resolver handles both, but a matching local handler on the raising controller takes precedence. ## Startup vs runtime failures - **Duplicate same-type registration** (two handlers for the exact same exception in one class) fails when `ExceptionHandlerMethodResolver` is built. - **Ambiguous best-match at request time** (two types tied at equal depth) throws `IllegalStateException` during resolution for that request. ## What local @ExceptionHandler CANNOT catch - **Servlet `Filter` exceptions** — filters run *before/around* `DispatcherServlet`, outside its handler execution, so the MVC resolvers never see them. They go to the container error page. - **Exceptions thrown by the `@ExceptionHandler` method itself** — these are **not** re-routed to another `@ExceptionHandler`; they propagate out of the resolver. - **Errors after the response is committed** — once bytes/headers are flushed, Spring cannot change the status; the resolver can only log. - **Exceptions during view rendering** in some cases, and async-dispatch errors after timeout/commit, have their own handling paths. - **Exceptions from other controllers** — local scope only. - **`HandlerInterceptor.afterCompletion`** exceptions and similar late-lifecycle throws are not funneled to `@ExceptionHandler`. ## Fall-through when nothing matches If no `@ExceptionHandler` (local or advice) matches and the other resolvers don't handle it either, `DispatcherServlet` rethrows; the Servlet container then performs an **error dispatch to `/error`** (in Spring Boot, `BasicErrorController`/`ErrorPage`). ## Why this matters (principal lens) - Understanding the resolver **ordering** lets you reason about why a `ResponseStatusException` (handled by `ResponseStatusExceptionResolver`) still works without a custom handler, and why your `@ExceptionHandler` for a superclass might unexpectedly win via **cause traversal**. - Knowing the **boundaries** (filters, commit, self-throw) tells you where you still need a Servlet-level `ErrorPage`/filter-level try-catch or an `@ControllerAdvice` fallback for a complete error contract. - Caching of `ExceptionHandlerMethodResolver` per class means resolution is cheap at runtime; the reflection cost is paid once.

  • Why can't a local @ExceptionHandler catch an exception thrown by a Servlet Filter?
    Filters execute outside DispatcherServlet, before/around its handler execution. The MVC HandlerExceptionResolver chain only sees exceptions from handler invocation inside DispatcherServlet, so filter exceptions bypass it and hit the container error page.
  • If your @ExceptionHandler itself throws, does another @ExceptionHandler catch it?
    No. An exception from within the handler is not re-dispatched through the resolvers; it propagates out and typically ends at the container /error. Keep handler bodies defensive.
  • How does a plain ResponseStatusException get a proper status without any @ExceptionHandler?
    The ResponseStatusExceptionResolver, later in the resolver chain, handles ResponseStatusException (and @ResponseStatus-annotated exceptions) by applying their status even when no @ExceptionHandler matched.

saying these in an interview costs you the question

  • Claiming @ExceptionHandler catches Servlet Filter or post-commit errors
  • Thinking a handler's own thrown exception is re-handled by another handler
  • Believing resolution re-scans the class per request (it's cached)
  • Assuming ExceptionHandlerExceptionResolver is the only resolver (there's a chain)

context