How is the order of HandlerExceptionResolvers determined, and what happens if you register your own resolver?
answer
- AnnotationAwareOrderComparator, lowest order first
- EHER=0, RSER=1, DHER=LOWEST_PRECEDENCE
- extend = append (safe); configure = replace (loses defaults)
- custom bean order 0 runs before the composite
- unknown exception → return null, never throw
basics
~20 sResolvers are sorted by their Ordered value and tried lowest-order-first; the first non-null result wins. The three defaults are ExceptionHandlerExceptionResolver (0), ResponseStatusExceptionResolver (1), DefaultHandlerExceptionResolver (lowest precedence). Use extendHandlerExceptionResolvers to add yours while keeping the defaults.
solid answer
~40 sDispatcherServlet holds an ordered List<HandlerExceptionResolver>, sorted by the Ordered interface / @Order (AnnotationAwareOrderComparator), and invokes them lowest-order-first, stopping at the first non-null ModelAndView. The defaults have deterministic orders: ExceptionHandlerExceptionResolver=0, ResponseStatusExceptionResolver=1, DefaultHandlerExceptionResolver=LOWEST_PRECEDENCE. To customize, a WebMvcConfigurer offers two hooks: configureHandlerExceptionResolvers(list) REPLACES the defaults entirely (dangerous — you lose @ExceptionHandler support and framework mappings unless you re-add them), while extendHandlerExceptionResolvers(list) APPENDS to the existing defaults. To insert a custom resolver ahead of the defaults, register it as an @Bean/@Component implementing Ordered with a low order (e.g., HIGHEST_PRECEDENCE) — DispatcherServlet auto-detects HandlerExceptionResolver beans and sorts them. The common mistake is defining a bare resolver bean and finding it silently replaces or reorders the defaults unexpectedly.
code
java · 26 lines@Configuration
class WebConfig implements WebMvcConfigurer {
// SAFE: keep the three defaults, add ours (runs LAST, only if defaults passed)
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(0, new LoggingExceptionResolver()); // index 0 -> runs FIRST
}
}
class LoggingExceptionResolver implements HandlerExceptionResolver, Ordered {
@Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
@Override
public ModelAndView resolveException(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
// side effect only, then delegate onward:
log.warn("handler {} threw {}", handler, ex.toString());
return null; // MUST return null so the real resolvers still run
}
}
// DANGEROUS anti-pattern — this REPLACES the defaults:
// @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> r) {
// r.add(new MyResolver()); // @ExceptionHandler + 405/415 mappings now GONE
// }go deeper
Know the chain is ordered and first-match wins; details optional.
Recall the three default orders and that first non-null result wins.
Distinguish extend vs configure hooks and place a custom resolver correctly relative to the defaults.
Reason about detectAll bean sorting vs the composite, and the operational risks of accidentally dropping @ExceptionHandler support.
## How the list is built and sorted `DispatcherServlet.initHandlerExceptionResolvers()` decides the list two ways: - **detectAllHandlerExceptionResolvers = true (default):** it finds *all* `HandlerExceptionResolver` beans in the context and sorts them with `AnnotationAwareOrderComparator` — which honors the `Ordered` interface, `@Order`, and `@Priority`. Lower value = higher priority = tried first. - **false:** it looks up a single bean named `handlerExceptionResolver`. With `@EnableWebMvc`/Boot, `WebMvcConfigurationSupport` contributes one `HandlerExceptionResolverComposite` bean (named `handlerExceptionResolver`) wrapping the three defaults **in fixed list order** — the composite iterates its internal list as-is, it does *not* re-sort. ## The default orders Inside `addDefaultHandlerExceptionResolvers`: - `ExceptionHandlerExceptionResolver` → `setOrder(0)` - `ResponseStatusExceptionResolver` → `setOrder(1)` (was set relative to the previous) - `DefaultHandlerExceptionResolver` → inherits `AbstractHandlerExceptionResolver`'s default `Ordered.LOWEST_PRECEDENCE`. So custom `@ExceptionHandler` beats `@ResponseStatus` beats framework defaults — by construction. ## Two WebMvcConfigurer hooks (know the difference) ```java @Configuration class WebConfig implements WebMvcConfigurer { // APPENDS to the defaults — safe @Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> r) { r.add(new MyResolver()); } // REPLACES the defaults — you lose @ExceptionHandler + framework mappings @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> r) { r.add(new MyResolver()); // now the ONLY resolver } } ``` - `extendHandlerExceptionResolvers` runs *after* defaults are added, so your resolver is appended (tried **last**, after the defaults). - `configureHandlerExceptionResolvers` is called *instead of* adding defaults; if you populate the list, Spring will **not** add the three defaults. Leaving it empty is the signal to use defaults. ## Putting a resolver FIRST Appending via `extend...` puts you last. To run *before* the defaults you can: - Register it as a top-level `@Bean` implementing `Ordered` with a very low order (with `detectAll=true` it is sorted alongside the composite). Note: the composite bean's order is that of the composite as a whole (`LOWEST_PRECEDENCE` by default), so a bean with order 0 runs before the whole default composite. - Or insert directly at index 0 in `extendHandlerExceptionResolvers` (`r.add(0, new MyResolver())`). ## Gotchas that bite people - **Silent default loss:** implementing `configureHandlerExceptionResolvers` and adding one resolver removes `@ExceptionHandler` support globally. Symptom: `@ControllerAdvice` stops working. - **Ordering surprise with @Order on a bean:** a custom resolver bean with `@Order(HIGHEST_PRECEDENCE)` will intercept exceptions before `ExceptionHandlerExceptionResolver`, potentially hiding your `@ExceptionHandler`s. - **Return null, not throw:** a custom resolver that doesn't recognize an exception must return `null` (delegate onward). Throwing from a resolver escapes the chain. - The composite short-circuits on first non-null; a resolver returning an empty ModelAndView by mistake (instead of null) will swallow exceptions it shouldn't.
- You added a resolver via extendHandlerExceptionResolvers and it never fires. Why?extend appends to the end, so it runs after the three defaults. If ExceptionHandlerExceptionResolver or DefaultHandlerExceptionResolver already resolves the exception, the chain short-circuits before reaching yours. Insert at index 0 or give it a lower Ordered value.
- A teammate reports @ControllerAdvice stopped working after a config change. What is the likely cause?They implemented configureHandlerExceptionResolvers and populated the list, which replaces the defaults — including ExceptionHandlerExceptionResolver. Switch to extendHandlerExceptionResolvers, or re-add the defaults.
saying these in an interview costs you the question
- Saying higher Ordered value means tried first (it is the opposite — lower = first)
- Believing configureHandlerExceptionResolvers keeps the defaults when you add a resolver
- A custom resolver that throws (instead of returning null) for exceptions it doesn't handle