Spring Framework
Exception Handling
DefaultHandlerExceptionResolver
Java
Customization

How to customize DefaultHandlerExceptionResolver logic?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

DefaultHandlerExceptionResolver is one of Spring MVC's built-in exception resolvers. Its job is to translate common framework exceptions into HTTP status codes, not to produce rich API error bodies. When teams want to change that behavior, the right answer is usually to add custom handling around it rather than to replace Spring defaults blindly.

What DefaultHandlerExceptionResolver Actually Does

Spring MVC resolves exceptions through an ordered chain. Several components may participate:

  • methods annotated with @ExceptionHandler
  • '@ControllerAdvice or @RestControllerAdvice'
  • 'ResponseStatusExceptionResolver'
  • 'DefaultHandlerExceptionResolver'
  • custom HandlerExceptionResolver beans

DefaultHandlerExceptionResolver focuses on framework exceptions such as unsupported HTTP methods, missing parameters, or media-type mismatches. It mainly sets status codes and may leave the response body empty. That is why many REST APIs need customization on top of it.

Before changing anything, decide what you are trying to customize:

  • status code mapping
  • response body format
  • logging behavior
  • resolver order

The implementation path depends on that choice.

Prefer @RestControllerAdvice for API Response Bodies

If the real goal is "return consistent JSON for framework exceptions," start with @RestControllerAdvice. It is clearer than subclassing the default resolver and easier to test.

java
1import org.springframework.http.HttpStatus;
2import org.springframework.http.ResponseEntity;
3import org.springframework.web.bind.MethodArgumentNotValidException;
4import org.springframework.web.bind.annotation.ExceptionHandler;
5import org.springframework.web.bind.annotation.RestControllerAdvice;
6
7import java.util.LinkedHashMap;
8import java.util.Map;
9
10@RestControllerAdvice
11public class ApiExceptionHandler {
12
13    @ExceptionHandler(MethodArgumentNotValidException.class)
14    public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
15        Map<String, Object> body = new LinkedHashMap<>();
16        body.put("error", "validation_failed");
17        body.put("message", "Request validation failed");
18        body.put("status", 400);
19        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
20    }
21}

This does not modify DefaultHandlerExceptionResolver itself, but it often solves the business problem more cleanly.

Subclass the Resolver When You Need Framework-Level Control

If you specifically need to change how one of the built-in framework exceptions is handled, subclass DefaultHandlerExceptionResolver and override the matching handle... method.

java
1import jakarta.servlet.http.HttpServletRequest;
2import jakarta.servlet.http.HttpServletResponse;
3import org.springframework.core.Ordered;
4import org.springframework.stereotype.Component;
5import org.springframework.web.HttpRequestMethodNotSupportedException;
6import org.springframework.web.servlet.ModelAndView;
7import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
8
9import java.io.IOException;
10
11@Component
12public class CustomDefaultHandlerExceptionResolver extends DefaultHandlerExceptionResolver {
13
14    public CustomDefaultHandlerExceptionResolver() {
15        setOrder(Ordered.LOWEST_PRECEDENCE - 10);
16    }
17
18    @Override
19    protected ModelAndView handleHttpRequestMethodNotSupported(
20            HttpRequestMethodNotSupportedException ex,
21            HttpServletRequest request,
22            HttpServletResponse response,
23            Object handler) throws IOException {
24
25        response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
26        response.setContentType("application/json");
27        response.getWriter().write("{\"error\":\"method_not_allowed\",\"message\":\"Use a supported HTTP method\"}");
28        return new ModelAndView();
29    }
30}

Returning a non-null ModelAndView tells Spring that the exception has been handled. If you return null, later resolvers may still run.

Registering Order Explicitly

Resolver order matters. If your custom resolver runs too late, Spring's built-in resolver may already have handled the exception. If it runs too early, it may intercept exceptions you intended to leave to a controller advice.

One explicit way to control the chain is through WebMvcConfigurer.

java
1import org.springframework.context.annotation.Configuration;
2import org.springframework.web.servlet.HandlerExceptionResolver;
3import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
4
5import java.util.List;
6
7@Configuration
8public class WebConfig implements WebMvcConfigurer {
9
10    private final CustomDefaultHandlerExceptionResolver customResolver;
11
12    public WebConfig(CustomDefaultHandlerExceptionResolver customResolver) {
13        this.customResolver = customResolver;
14    }
15
16    @Override
17    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
18        resolvers.add(0, customResolver);
19    }
20}

Use this kind of registration sparingly. A resolver chain with unclear ordering becomes difficult to reason about after framework upgrades.

When ResponseEntityExceptionHandler Is Better

For REST APIs, extending ResponseEntityExceptionHandler is often the best middle ground. It already understands many MVC exceptions and lets you customize the HTTP body without writing directly to HttpServletResponse.

That approach is usually easier to maintain than overriding DefaultHandlerExceptionResolver, because the returned ResponseEntity is explicit and test-friendly.

Testing the Behavior

Exception customization should be covered by integration tests, not only unit tests. The key risks are chain order and response shape.

java
1import org.junit.jupiter.api.Test;
2import org.springframework.beans.factory.annotation.Autowired;
3import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
4import org.springframework.boot.test.context.SpringBootTest;
5import org.springframework.test.web.servlet.MockMvc;
6
7import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
8import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
9import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
10
11@SpringBootTest
12@AutoConfigureMockMvc
13class ResolverIntegrationTest {
14
15    @Autowired
16    private MockMvc mockMvc;
17
18    @Test
19    void unsupportedMethodReturnsCustomJson() throws Exception {
20        mockMvc.perform(post("/api/read-only"))
21                .andExpect(status().isMethodNotAllowed())
22                .andExpect(content().string(org.hamcrest.Matchers.containsString("method_not_allowed")));
23    }
24}

A test like this catches regressions where resolver order changes silently.

Common Pitfalls

The first mistake is trying to customize response bodies by replacing Spring's default resolver entirely. In most API projects, @RestControllerAdvice or ResponseEntityExceptionHandler is the simpler tool.

Another problem is unclear ordering. Exception handling bugs often come from the wrong resolver winning the chain rather than from incorrect business logic.

A third issue is writing to HttpServletResponse and still letting processing continue. Once your resolver writes and returns a handled ModelAndView, the chain should stop for that exception.

Finally, teams sometimes add custom logging in every resolver and end up logging the same failure multiple times. Decide which layer owns error logging and keep it consistent.

Summary

  • Use @RestControllerAdvice first when the goal is structured API error bodies.
  • Subclass DefaultHandlerExceptionResolver only when you need framework-level exception handling control.
  • Set resolver order explicitly so the right handler wins.
  • Prefer ResponseEntityExceptionHandler when you want Spring-aware customization without manual response writing.
  • Verify behavior with integration tests that assert both status codes and response bodies.

Course illustration
Course illustration

All Rights Reserved.