skip to content

Explain what DefaultHandlerExceptionResolver and ResponseStatusExceptionResolver do, including the body/status subtleties.

level: seniorimportance: should knowfreq 26%

answer

  1. DHER: framework exceptions → status codes (405/415/406/400/404)
  2. sets status + headers, empty ModelAndView, no custom body
  3. RSER: @ResponseStatus OR ResponseStatusException
  4. reason present → sendError → /error page
  5. Spring 6: ErrorResponse + ProblemDetail (rendered by EHER)

basics

~20 s

DefaultHandlerExceptionResolver maps Spring MVC's built-in exceptions (like 405, 415, 400) to status codes. ResponseStatusExceptionResolver applies the status from @ResponseStatus-annotated exceptions or a ResponseStatusException. Both mainly set the status; they don't produce a custom response body.

solid answer

~40 s

DefaultHandlerExceptionResolver is the framework's fallback: it recognizes Spring MVC's own exceptions and translates them to HTTP statuses — HttpRequestMethodNotSupportedException→405 (with an Allow header), HttpMediaTypeNotSupportedException→415, HttpMediaTypeNotAcceptableException→406, MissingServletRequestParameterException/MethodArgumentNotValidException→400, NoHandlerFoundException→404, MissingPathVariableException→500, etc. ResponseStatusExceptionResolver handles @ResponseStatus-annotated exception classes and ResponseStatusException instances, applying their status/reason. The subtlety: when a reason is present, ResponseStatusExceptionResolver calls response.sendError(status, reason), which commits an error and delegates body generation to the container's error page (e.g., /error in Boot); with no reason it uses setStatus. Neither resolver writes your own JSON body — DefaultHandlerExceptionResolver only sets status (Boot's /error fills the body). To control the body, handle the exception with @ExceptionHandler or subclass ResponseEntityExceptionHandler. In Spring 6 these framework exceptions implement ErrorResponse and carry a ProblemDetail.

code

java · 20 lines
java
// ResponseStatusExceptionResolver handles BOTH of these:

// (a) annotation on the exception type
@ResponseStatus(HttpStatus.NOT_FOUND) // NOTE: no 'reason' -> setStatus, your body kept
class OrderNotFoundException extends RuntimeException {
    OrderNotFoundException(String id) { super("order " + id); }
}

// (b) thrown inline, status decided per call site
@GetMapping("/orders/{id}")
Order get(@PathVariable String id) {
    return repo.find(id)
        .orElseThrow(() -> new ResponseStatusException(
            HttpStatus.NOT_FOUND, "order " + id + " not found"));
    // reason present -> sendError -> container /error page in Spring Boot
}

// DefaultHandlerExceptionResolver fires automatically, no code:
//   POST body that fails JSON parsing -> HttpMessageNotReadableException -> 400
//   wrong Content-Type -> HttpMediaTypeNotSupportedException -> 415

go deeper

for a junior

Know DefaultHandlerExceptionResolver gives correct status codes for framework errors automatically.

for a middle

Recall the key mappings and that ResponseStatusExceptionResolver handles @ResponseStatus and ResponseStatusException.

for a senior

Explain the sendError-vs-setStatus body implication and how to override the body via @ExceptionHandler.

for a principal

Connect to Spring 6 ErrorResponse/ProblemDetail and ResponseEntityExceptionHandler, and reason about content negotiation on error responses.

## DefaultHandlerExceptionResolver — the framework's safety net This resolver exists so Spring MVC's *own* exceptions map to correct status codes even if you write no error handling. It is essentially a big type dispatch (`doResolveException` → `handleXxx` methods). Key mappings: | Exception (thrown by Spring MVC internals) | Status | Notes | |---|---|---| | `HttpRequestMethodNotSupportedException` | 405 | sets `Allow` header | | `HttpMediaTypeNotSupportedException` | 415 | sets `Accept` header | | `HttpMediaTypeNotAcceptableException` | 406 | | | `MissingPathVariableException` | 500 | server bug, not client | | `MissingServletRequestParameterException` | 400 | | | `MissingServletRequestPartException` | 400 | | | `ServletRequestBindingException` | 400 | | | `MethodArgumentNotValidException` | 400 | @Valid body failure | | `HandlerMethodValidationException` | 400 | (Spring 6.1) | | `NoHandlerFoundException` | 404 | needs throwExceptionIfNoHandlerFound | | `NoResourceFoundException` | 404 | (Spring 6.1 static resources) | | `ConversionNotSupportedException` | 500 | | | `TypeMismatchException` | 400 | | | `HttpMessageNotReadableException` | 400 | malformed body | | `HttpMessageNotWritableException` | 500 | | | `AsyncRequestTimeoutException` | 503 | | It sets the status/headers and returns an **empty ModelAndView**. It does **not** write a JSON body describing the error — in Spring Boot the body then comes from the `/error` mapping. ## ResponseStatusExceptionResolver Two trigger mechanisms: 1. **`@ResponseStatus`** on the exception class: ```java @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Order not found") class OrderNotFoundException extends RuntimeException {} ``` 2. **`ResponseStatusException`** (Spring 5+), status passed at throw time — no annotation needed, so you can vary the status per call site: ```java throw new ResponseStatusException(HttpStatus.CONFLICT, "already exists"); ``` It reads the status (and optional reason), applies it, and returns an empty ModelAndView. ### The sendError vs setStatus subtlety - If a **reason** is supplied, the resolver calls `response.sendError(statusCode, reason)`. `sendError` commits the response and hands off to the container's error mechanism — in Boot that is an ERROR dispatch to `/error`, so the reason may appear in the error page's `message` field, and your own controller body is discarded. - If **no reason**, it calls `response.setStatus(statusCode)` and the response stays under your control. This is why `@ResponseStatus(reason="...")` sometimes produces a different body shape than you expect: it routes through the container error page. ## Spring 6 / ProblemDetail In Spring Framework 6, most of the DefaultHandlerExceptionResolver exceptions implement **`ErrorResponse`** (many extend `ErrorResponseException`) and expose a **`ProblemDetail`** (RFC 7807/9457). Combined with `ResponseEntityExceptionHandler` (a `@ControllerAdvice` base class) they can render structured `application/problem+json` bodies. But that rendering happens through `ExceptionHandlerExceptionResolver`, not DefaultHandlerExceptionResolver — the latter still just sets status. ## When to use / override - Rely on DefaultHandlerExceptionResolver for correct **status codes** for free. - To also control the **body** of those framework errors, add `@ExceptionHandler` methods (which run first via ExceptionHandlerExceptionResolver) or extend `ResponseEntityExceptionHandler` and override the relevant `handleXxx`. - Use `ResponseStatusException` for quick, per-call status without a bespoke exception class; use `@ResponseStatus` when the exception type always maps to one status. ## Gotchas - `@ResponseStatus(reason=...)` → container error page (sendError). Omit reason to keep your body. - DefaultHandlerExceptionResolver gives status but no descriptive body by itself. - `NoHandlerFoundException` only fires if configured to throw; otherwise 404 is produced without this resolver.

  • Why might @ResponseStatus(reason="...") produce a different response body than you set in a @ResponseBody method?
    Because with a reason present, ResponseStatusExceptionResolver calls response.sendError, which commits and delegates to the container's error page (/error in Boot). Your controller body is discarded and the error page renders instead.
  • DefaultHandlerExceptionResolver returns a 400 but an empty/default body. How do you add a structured problem+json body for that same exception?
    Handle it earlier in the chain: write an @ExceptionHandler (or override the matching handleXxx in a ResponseEntityExceptionHandler subclass). ExceptionHandlerExceptionResolver runs first and can return a ProblemDetail/ResponseEntity body.

saying these in an interview costs you the question

  • Claiming DefaultHandlerExceptionResolver writes a descriptive JSON body itself
  • Thinking ResponseStatusException needs the @ResponseStatus annotation
  • Not knowing that a reason triggers sendError and the container error page

context