Exception Handling in Spring Web: ControllerAdvice, ExceptionHandlers, and Error Responses
Understanding HandlerExceptionResolver composite pipelines, RFC 7807 ProblemDetail specifications, and security leaks
Part 13 in Series — Catch up on the previous article: Spring MVC Request Lifecycle: HandlerMappings, HandlerAdapters, and MessageConverters (Part 12) before diving into this post.
Why You Need This in Real Life
When a web application suffers a database connection timeout during a payment transaction, the lack of centralized exception handling causes Spring Boot to return Tomcat’s default HTML Whitelabel Error Page or raw JSON stack traces:
{
"timestamp": "2026-09-08T18:30:00.123+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "org.postgresql.util.PSQLException: Cannot connect to DB at 10.0.4.12:5432\n\tat org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:315)\n\tat org.postgresql.core.v3.ConnectionFactoryImpl.openConnection(ConnectionFactoryImpl.java:58)...",
"path": "/api/v1/payments"
}
This raw stack trace exposes internal IP addresses, database vendor types, schema structures, and class path paths to potential attackers (OWASP Category A05: Security Misconfiguration / Information Disclosure CWE-209). Simultaneously, client applications receiving raw HTML cannot parse the error response as structured JSON, breaking mobile and web UI integrations.
To build secure, resilient microservices, you must understand how Spring MVC catches exceptions during doDispatch() using HandlerExceptionResolver and how to build centralized error handling using @ControllerAdvice.
Part 1: The HandlerExceptionResolver Hierarchy
When an exception is thrown inside a @Controller method, an interceptor, or an argument resolver, DispatcherServlet catches the Throwable inside doDispatch() and delegates it to processHandlerException():
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// Iterate through registered HandlerExceptionResolvers
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break; // First resolver to return a non-null ModelAndView wins!
}
}
}
...
}
Order of Registered Exception Resolvers
Spring MVC registers a composite resolver (HandlerExceptionResolverComposite) containing three primary resolvers executed in strict precedence order:
+-----------------------------------------------------------------------------+
| HandlerExceptionResolverComposite Pipeline |
| |
| Exception Thrown in Request Lifecycle |
| | |
| v |
| 1. ExceptionHandlerExceptionResolver (Order: 0) |
| - Checks for matching @ExceptionHandler methods in @ControllerAdvice |
| or local @Controller class. |
| | (If unhandled / returns null) |
| v |
| 2. ResponseStatusExceptionResolver (Order: 1) |
| - Checks if exception class has @ResponseStatus or is |
| ResponseStatusException instance. |
| | (If unhandled / returns null) |
| v |
| 3. DefaultHandlerExceptionResolver (Order: 2) |
| - Translates standard Spring MVC exceptions (e.g., |
| HttpRequestMethodNotSupportedException -> HTTP 405 Method Not Allowed)|
+-----------------------------------------------------------------------------+
Part 2: Building Global Exception Handlers with @RestControllerAdvice
@ControllerAdvice (and its REST specialized counterpart @RestControllerAdvice) uses component scanning to register a global interceptor for exceptions across all controllers.
Standard Production Implementation
package com.example.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. Handle Domain Validation Errors (HTTP 400)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationError(MethodArgumentNotValidException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Invalid request payload attributes");
problemDetail.setTitle("Field Validation Error");
problemDetail.setType(URI.create("https://api.example.com/errors/validation-error"));
problemDetail.setProperty("timestamp", Instant.now());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
problemDetail.setProperty("invalidFields", errors);
return problemDetail;
}
// 2. Handle Resource Not Found Errors (HTTP 404)
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
problemDetail.setTitle("Resource Not Found");
problemDetail.setType(URI.create("https://api.example.com/errors/not-found"));
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
// 3. Catch-All Fallback for Unexpected Internal System Failures (HTTP 500)
@ExceptionHandler(Exception.class)
public ProblemDetail handleUnexpectedException(Exception ex) {
// Log stack trace internally for engineering diagnostics
org.slf4j.LoggerFactory.getLogger(GlobalExceptionHandler.class)
.error("Unhandled internal server exception", ex);
// Return sanitized error response to client WITHOUT leaking internals
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred. Please contact support.");
problemDetail.setTitle("Internal Server Error");
problemDetail.setType(URI.create("https://api.example.com/errors/internal-error"));
problemDetail.setProperty("timestamp", Instant.now());
return problemDetail;
}
}
Part 3: Modern RFC 7807 Problem Details Standard
Starting in Spring 6 and Spring Boot 3, Spring natively adopted the RFC 7807 Problem Details for HTTP APIs specification (org.springframework.http.ProblemDetail).
Instead of returning ad-hoc JSON structures across microservices, RFC 7807 standardizes error fields:
{
"type": "https://api.example.com/errors/validation-error",
"title": "Field Validation Error",
"status": 400,
"detail": "Invalid request payload attributes",
"instance": "/api/v1/orders",
"timestamp": "2026-09-08T18:45:00Z",
"invalidFields": {
"amount": "must be greater than 0",
"currency": "must not be blank"
}
}
To enable RFC 7807 standard responses for built-in Spring MVC exceptions globally, set spring.mvc.problemdetails.enabled=true in application.properties.
Part 4: Production Gotchas & Security Pitfalls
Gotcha 1: Swallowing Exceptions in Filter Chains
@ControllerAdvice only catches exceptions thrown inside Spring MVC’s servlet processing loop (DispatcherServlet). Exceptions thrown in Servlet Filter instances (such as Spring Security’s JwtAuthenticationFilter) occur before DispatcherServlet is reached.
- Symptom: An unhandled exception in a custom
Filterbypasses@ControllerAdviceand returns Tomcat’s raw Whitelabel Error Page. - Solution: Catch exceptions in the custom
Filterexplicitly and delegate them to Spring’sHandlerExceptionResolver:@Component public class JwtFilter extends OncePerRequestFilter { @Autowired @Qualifier("handlerExceptionResolver") private HandlerExceptionResolver resolver; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) { try { chain.doFilter(req, res); } catch (Exception ex) { resolver.resolveException(req, res, null, ex); // Delegates to @ControllerAdvice! } } }
Gotcha 2: High-Precedence Specificity Ambiguity
If multiple @ExceptionHandler methods match an exception hierarchy (e.g., IllegalArgumentException vs Exception), ExceptionHandlerExceptionResolver selects the handler for the most specific exception type. However, if two handlers are defined for the exact same exception class across two different @ControllerAdvice classes, the selection order becomes non-deterministic unless @Order is specified on the advice classes.
Next Steps
Having covered Spring Web request routing and exception management, we now transition to Module 5 (Data Access & Transactions): starting with how Spring abstracts raw JDBC primitives into JdbcTemplate.
References & Further Reading
- Spring.io. Spring Security Reference Architecture — The Security Filter Chain. Spring Security Docs.
- OWASP Foundation. OWASP Top 10 Security Risks — A07: Identification and Authentication Failures. OWASP.
- Winch, R., et al. (2023). Spring Security Reference Manual. VMware Tanzu.
Part 14: Data Access Primitives: From Plain JDBC to Spring JdbcTemplate
Continue to Part 14 →