skip to content

How is the order of HandlerExceptionResolvers determined, and what happens if you register your own resolver?

level: seniorimportance: should knowfreq 32%

answer

  1. AnnotationAwareOrderComparator, lowest order first
  2. EHER=0, RSER=1, DHER=LOWEST_PRECEDENCE
  3. extend = append (safe); configure = replace (loses defaults)
  4. custom bean order 0 runs before the composite
  5. unknown exception → return null, never throw

basics

~20 s

Resolvers 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 s

DispatcherServlet 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
java
@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

for a junior

Know the chain is ordered and first-match wins; details optional.

for a middle

Recall the three default orders and that first non-null result wins.

for a senior

Distinguish extend vs configure hooks and place a custom resolver correctly relative to the defaults.

for a principal

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

context