Spring Boot Exception Handling: @ControllerAdvice, @ExceptionHandler, ErrorAttributes

Master Spring Boot exception handling with @ControllerAdvice, @ExceptionHandler, ErrorAttributes, and security best practices for production APIs.

published: reading time: 26 min read author: GeekWorkBench
Quick Summary

Master Spring Boot exception handling with @ControllerAdvice, @ExceptionHandler, ErrorAttributes, and security best practices for production APIs. The guide uses practical examples to explain why exception handling matters, the exception handling flow and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Spring Boot Exception Handling: @ControllerAdvice, @ExceptionHandler, ErrorAttributes

Every REST API crashes eventually. A database connection drops, a validation check fails, an unexpected null pointer bubbles up. What separates professional APIs from amateur ones is not whether they fail, but how they handle failure. Spring Boot gives you a powerful toolkit for exception handling, and in this post, I will walk you through every piece of it.

Why Exception Handling Matters

Introduction

Exception handling in Spring Boot turns failures into consistent, useful HTTP responses instead of leaking implementation details or leaving clients to interpret arbitrary errors. This guide covers controller-level and global handlers, response formats, validation failures, exception mapping, logging, and the trade-offs between local recovery and centralized handling.

The Exception Handling Flow

Understanding how Spring Boot processes exceptions is essential before diving into the specific annotations.

graph TD
    A[HTTP Request] --> B{Controller Method}
    B -->|Success| C[Return Response]
    B -->|Throws Exception| D[DispatcherServlet]
    D --> E["@ExceptionHandler Methods"]
    E --> F["@ControllerAdvice Global Handler"]
    F --> G{"Exception Type Match?"}
    G -->|Yes| H[Handle Exception]
    G -->|No| I[Base Exception Handlers]
    H --> J[ErrorAttributes Applied]
    I --> J
    J --> K[HTTP Response]
    C --> K

When an exception is thrown inside a controller, Spring’s DispatcherServlet catches it and searches for a matching @ExceptionHandler method. If the controller does not define one, the search expands to any @ControllerAdvice bean in the application context. The matched handler processes the exception, optionally modifies the response using ErrorAttributes, and returns the final HTTP response.

@ExceptionHandler: Per-Controller Control

The @ExceptionHandler annotation lets you define exception handling logic directly inside a controller. This works well for controller-specific error responses.

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "USER_NOT_FOUND",
            ex.getMessage(),
            HttpStatus.NOT_FOUND.value()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

The method signature of @ExceptionHandler is flexible. Spring automatically injects the thrown exception, the HTTP request, and even the WebRequest if you declare them as parameters.

When to Use @ExceptionHandler

  • Controller-specific error responses that do not apply elsewhere
  • Keeping error handling close to the code that produces the errors
  • Quick prototyping before extracting to a global handler

When NOT to Use @ExceptionHandler

  • Business exceptions that multiple controllers need to handle the same way
  • When you find yourself copying the same handler method across controllers
  • For exceptions that should be handled uniformly across the entire API

Copying handler methods across controllers is a maintenance nightmare. If you need to change the error format in one place, you have to change it everywhere.

@ControllerAdvice: Global Exception Handling

The @ControllerAdvice annotation transforms any bean into a global exception handler. All @ExceptionHandler methods in that class become applicable to every controller in the application.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ApiError> handleUserNotFound(UserNotFoundException ex) {
        ApiError error = new ApiError(
            "USER_NOT_FOUND",
            ex.getMessage(),
            Instant.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
        List<String> details = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .toList();

        ApiError error = new ApiError(
            "VALIDATION_ERROR",
            "Request validation failed",
            Instant.now(),
            details
        );
        return ResponseEntity.badRequest().body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiError> handleGeneric(Exception ex) {
        // Log the actual exception, never expose it
        log.error("Unhandled exception", ex);
        ApiError error = new ApiError(
            "INTERNAL_ERROR",
            "An unexpected error occurred",
            Instant.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Selective Application with Attributes

@ControllerAdvice is powerful, but sometimes you want it to apply only to specific controllers or packages.

// Apply only to controllers in this package
@ControllerAdvice("com.example.api.controllers")

// Apply only to controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)

// Apply only to specific controller classes
@ControllerAdvice(assignableTypes = {UserController.class, OrderController.class})

This granular control is useful in large applications where different modules need different error handling strategies.

@ResponseStatus: Declarative HTTP Status Codes

The simplest way to set an HTTP status on an exception is annotating the exception class itself with @ResponseStatus.

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(Long id) {
        super("User with id " + id + " not found");
    }
}

When this exception is thrown and not caught by any @ExceptionHandler, Spring automatically returns a 404 with an empty response body. It works, but it is limited. You cannot add a custom body, log the exception, or apply any additional logic.

Combining @ResponseStatus with @ExceptionHandler

A better pattern is using @ResponseStatus purely to declare intent on the exception class while handling the actual response construction in @ControllerAdvice.

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    private final String resourceName;
    private final String fieldName;
    private final Object fieldValue;

    public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s: '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }
    // getters
}

This separation of concerns keeps your exception classes focused on data while the handler focuses on response formatting.

Customizing Error Responses with ErrorAttributes

ErrorAttributes controls what data appears in the default error response. The default implementation includes timestamp, status, error, message, and path. You can customize or extend this.

Extending DefaultErrorAttributes

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);

        // Add custom fields
        errorAttributes.put("correlationId", UUID.randomUUID().toString());
        errorAttributes.put("version", "v1");

        // Remove fields you do not want to expose
        errorAttributes.remove("trace");

        return errorAttributes;
    }
}

Creating a Fully Custom ErrorAttributes Implementation

@Component
public class CustomErrorAttributes implements ErrorAttributes {

    private final ErrorLoggingService errorLoggingService;

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Throwable exception = getError(webRequest);

        Map<String, Object> errorAttributes = new HashMap<>();
        errorAttributes.put("timestamp", Instant.now().toString());
        errorAttributes.put("status", getStatus(webRequest).value());
        errorAttributes.put("error", getStatus(webRequest).getReasonPhrase());
        errorAttributes.put("message", exception != null ? exception.getMessage() : "No message");
        errorAttributes.put("path", webRequest.getDescription(false).replace("uri=", ""));

        if (options.isIncluded(Include.STACK_TRACE)) {
            // Only include stack trace in development
            if (environment.getActiveProfiles().contains("dev")) {
                errorAttributes.put("trace", getStackTrace(exception));
            }
        }

        // Always log for correlation
        if (exception != null) {
            String correlationId = errorLoggingService.log(exception);
            errorAttributes.put("correlationId", correlationId);
        }

        return errorAttributes;
    }

    @Override
    public Throwable getError(WebRequest webRequest) {
        return (Throwable) webRequest.getAttribute("javax.servlet.error.exception", SCOPE_REQUEST);
    }

    private HttpStatus getStatus(WebRequest request) {
        Integer status = (Integer) request.getAttribute("javax.servlet.error.status_code", SCOPE_REQUEST);
        return status != null ? HttpStatus.valueOf(status) : HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

Register your custom ErrorAttributes by declaring it as a @Component bean. Spring Boot’s ErrorMvcAutoConfiguration will automatically pick it up.

Failure Scenarios

Even with solid exception handling in place, things can still go wrong. Here are the failure scenarios you need to anticipate.

Handler Throws an Exception

If your @ExceptionHandler method itself throws an exception, Spring falls back to its default error handling. This creates a confusing situation where the original error is lost.

Mitigation: Wrap your handler logic in try-catch and have a fallback that returns a safe generic error.

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleSafe(Exception ex) {
    try {
        // Risky processing
        return processException(ex);
    } catch (Exception inner) {
        log.error("Error in exception handler", inner);
        return ResponseEntity.status(500).body(ApiError.generic());
    }
}

ResponseStatusException Loses Custom Body

ResponseStatusException is the programmatic equivalent of @ResponseStatus, but it does not automatically work with @ControllerAdvice exception matching.

// This will NOT be caught by @ExceptionHandler(ResponseStatusException.class)
// if it was thrown from within a @ExceptionHandler itself
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found");

Mitigation: Use custom exception classes instead of ResponseStatusException for anything that needs centralized handling.

Circular Exception Handling

If handler A throws exception B, and handler B throws exception A, you get a stack overflow.

Mitigation: Never throw exceptions from within @ExceptionHandler methods. Always convert them to ResponseEntity returns.

Missing Handler for Common Exceptions

MethodArgumentNotValidException, HttpMessageNotReadableException, and MissingServletRequestParameterException are thrown by Spring MVC automatically. If you do not handle them, clients get opaque 400 errors with no useful details.

Mitigation: Add handlers for these common validation exceptions in your global handler.

When to Use / When NOT to Use

When to Use @ExceptionHandler (Per-Controller)

Use @ExceptionHandler when an error response is specific to one controller and does not make sense anywhere else. If a particular controller needs to format an exception differently because the consumers of that endpoint expect a unique error structure, keep it local. This approach also works well during early prototyping before you know what the global pattern should look like.

When NOT to Use @ExceptionHandler (Per-Controller)

As soon as you find yourself copying the same @ExceptionHandler method to multiple controllers, refactor to @ControllerAdvice. Duplicated handler code is a maintenance liability — when the error format needs to change, you have to update every copy. If the same exception type needs to be handled the same way across multiple endpoints, that is a signal to move it global.

When to Use @ControllerAdvice (Global)

Use @ControllerAdvice for exceptions that make sense to handle uniformly across the entire API — business exceptions like UserNotFoundException, validation errors from MethodArgumentNotValidException, or any error format that should be consistent regardless of which controller originated the request.

When to Use @ResponseStatus on Exception Classes

Use @ResponseStatus on custom exception classes when you need a specific HTTP status code and nothing more. It is a zero-code way to map an exception to a status. Avoid it when you need a custom response body, when you want to log the exception, or when the error response depends on context that only a handler method can provide.

When to Use ErrorAttributes Customization

Use ErrorAttributes when you need every error response — including those from Spring’s default BasicErrorController — to follow a consistent structure across your entire API. If you want all clients to receive a uniform error envelope with fields like timestamp, status, error, message, and correlationId, customize ErrorAttributes rather than relying on per-handler construction.

When to Use ErrorController Implementation

Use a custom ErrorController when you need full control over the error page rendering or when the default JSON error format does not meet your needs at all. This is a lower-level approach than ErrorAttributes and is rarely needed.

Trade-off Table

Approach Pros Cons
@ExceptionHandler per controller Simple, local reasoning Code duplication across controllers
@ControllerAdvice global Single place to maintain May need splitting in large apps
@ResponseStatus on exception Declarative, zero code No custom body, limited logic
ErrorAttributes customization Consistent error format Involves Spring internals
ErrorController implementation Full control over error page More code, easier to get wrong

Security Considerations

This section is non-negotiable for production systems.

Never Leak Stack Traces

The default Spring Boot error page includes the full stack trace in development. If that page ever reaches a production client, you have disclosed your entire class hierarchy, library versions, and internal logic.

@Component
public class ProductionErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest request, ErrorAttributeOptions options) {
        Map<String, Object> attrs = super.getErrorAttributes(request, options);

        // Never expose trace in production
        if (!environment.getActiveProfiles().contains("dev")) {
            attrs.remove("trace");
            attrs.remove("errors");
        }

        return attrs;
    }
}

Validate Error Message Content

If you pass user input directly into error messages, you open the door to log injection attacks. A malicious user submits a request with X-Requested-With: <script>alert(1)</script> and if you echo that header into a log-prefixed error message, you have a cross-site scripting vector.

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ApiError> handleBadRequest(BadRequestException ex) {
    String sanitized = sanitize(ex.getMessage()); // Strip HTML/script tags
    return ResponseEntity.badRequest().body(new ApiError("BAD_REQUEST", sanitized));
}

Do Not Expose Internal Paths

The path field in error attributes reveals your internal URL structure. In some deployments, this alone can help an attacker map your application’s internal routing.

@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
    Map<String, Object> attrs = super.getErrorAttributes(webRequest, options);
    // Replace path with a generic indicator in production
    if (!environment.getActiveProfiles().contains("dev")) {
        attrs.put("path", "redacted");
    }
    return attrs;
}

Common Pitfalls / Anti-Patterns

Handling the wrong exception type. Make sure your @ExceptionHandler parameter matches the actual exception being thrown. Generic Exception.class handlers should always be last in the chain.

Ignoring the HTTP method context. A 404 on a GET request has different semantics than a 404 on a DELETE. Your error messages should reflect what failed, not just that it failed.

Returning null from a handler. If your handler returns null, Spring treats it as if no handler matched and continues searching. This leads to confusing behavior and potentially recursive handler searches.

Forgetting to mark exceptions as unchecked. Checked exceptions require throws declarations and clutter your controller signatures. Extend RuntimeException for most business exceptions so handlers can catch them without ceremony.

Not testing your error paths. Unit tests that only exercise the happy path miss the most critical part of your API. Write tests that verify each exception handler returns the correct status code and body.

Observability Checklist

A well-observed API lets you diagnose production issues in minutes, not hours.

  • Every exception handler logs the exception with a correlation ID
  • Error responses include the correlation ID so clients can report it
  • Metric counter incremented for each error type
  • Alerting configured for spike in 5xx errors
  • Alerting configured for spike in 4xx client errors (may indicate abuse)
  • Health check endpoint reports degraded state when error rate is high
  • Distributed trace ID propagated through error logging
  • Error attributes include request ID for log correlation
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleLoggedException(Exception ex, WebRequest request) {
    String correlationId = UUID.randomUUID().toString();
    String traceId = request.getHeader("X-Trace-Id");

    log.error("Unhandled exception. correlationId={}, traceId={}, exception={}",
        correlationId, traceId, ex.getClass().getName(), ex);

    metrics.increment("error.unhandled", "type", ex.getClass().getSimpleName());

    return ResponseEntity.status(500).body(
        new ApiError("INTERNAL_ERROR", "An error occurred. Reference: " + correlationId)
    );
}

Quick Recap Checklist

  • Use @ControllerAdvice for global exception handling across all controllers
  • Reserve @ExceptionHandler for controller-specific cases only
  • Separate exception classes from response formatting logic
  • Customize ErrorAttributes for consistent error response structure
  • Never expose stack traces, internal paths, or class names in production
  • Add correlation IDs to every error response
  • Handle Spring MVC’s automatic exceptions (MethodArgumentNotValidException, etc.)
  • Write handler methods that never throw exceptions
  • Test every error path with unit tests
  • Log exceptions server-side with enough context to reproduce the issue

Interview Questions

1. What is the difference between @ControllerAdvice and @RestControllerAdvice?

There is no functional difference. @RestControllerAdvice is simply a compound annotation that combines @ControllerAdvice with @ResponseBody. Under the hood, it is the same ControllerAdvice mechanism. The @ResponseBody annotation means that every return value from exception handler methods is automatically serialized to JSON, which is what you want for REST APIs. If you use plain @ControllerAdvice, you would need to wrap your return values in ResponseEntity to achieve the same effect.

2. How does Spring Boot's default error handling work when no @ExceptionHandler matches?

When no custom handler matches, Spring falls back to BasicErrorController, which is auto-configured by Spring Boot's ErrorMvcAutoConfiguration. This controller has two methods: one that renders an HTML error page for browser requests, and one that renders a JSON response for API clients. It uses DefaultErrorAttributes to build the error attributes, which includes properties like timestamp, status, error, message, and path. Understanding this fallback is important because if your custom handler throws an uncaught exception, the client may receive a response from BasicErrorController instead of your intended error format.

3. How would you implement different error formats for different clients (web vs mobile)?

You can inspect the HttpServletRequest in your @ExceptionHandler method and check the Accept header or other client indicators. Alternatively, use Spring's ContentNegotiationStrategy to determine the requested media type. For a cleaner implementation, create separate @ControllerAdvice classes targeted at different controller packages using the annotations or assignableTypes attribute. This way your web controllers get HTML-friendly errors while your mobile API controllers get compact JSON errors with no extraneous fields.

4. What is the purpose of ErrorAttributeOptions and when would you use it?

ErrorAttributeOptions controls which fields are included in the error response when you extend DefaultErrorAttributes. It has flags for INCLUDE_TIMESTAMP, INCLUDE_MESSAGE, INCLUDE_STACK_TRACE, INCLUDE_BINDING_ERRORS, and INCLUDE_EXCEPTION. You would use this when you need fine-grained control over the error response content. For example, you might want to exclude the stack trace in production while including it in development, or you might want to omit the message field for security-sensitive endpoints while keeping it for internal services.

5. How do you handle exceptions thrown by exception handlers themselves without creating infinite loops?

The golden rule is that exception handler methods should never throw exceptions. They should always return a ResponseEntity. If your handler logic itself needs to throw an exception (for example, if a service layer call fails while you are building an error response), wrap it in a try-catch and return a safe fallback response. A common pattern is to have a private helper method buildErrorResponse that is fully self-contained and guaranteed not to throw. Additionally, always order your exception handlers from most specific to least specific, with a generic Exception.class handler last as a safety net that should never trigger if your specific handlers are complete.

6. How does RFC 7807 Problem Details change the error response format in Spring Boot?

RFC 7807 defines a standardized error response format with five fields: type (a URI identifying the problem type), title (a short human-readable description), status (the HTTP status code), detail (a human-readable explanation specific to this occurrence), and instance (a URI that identifies the specific occurrence). Spring Boot supports this natively through ProblemDetail and ErrorResponse. You can return ProblemDetail directly from your exception handler, and Spring will serialize it correctly. The advantage over ad-hoc error responses is that clients can programmatically parse the type URI to understand the error category without relying on fragile string matching of error messages.

7. What is the difference between MethodArgumentNotValidException and ConstraintViolationException?

MethodArgumentNotValidException is thrown when bean validation on a request body fails, typically from @Valid annotated parameters in @RequestBody methods. ConstraintViolationException is thrown when validation fails on method parameters or path variables, typically from @Validated at the class level with @Valid on method parameters. The key practical difference is that MethodArgumentNotValidException gives you access to FieldError objects through getBindingResult(), letting you return per-field error messages. ConstraintViolationException gives you a set of ConstraintViolation objects that describe which parameter failed and why. Both need to be handled separately in your @ControllerAdvice.

8. How does @Transactional interact with exception handling in Spring?

By default, Spring's transaction infrastructure rolls back only on unchecked exceptions (RuntimeException and its subclasses) and Error. Checked exceptions do not trigger rollback by default. If your exception handler catches an exception that occurs within a transactional method, the transaction has already been rolled back by the time your handler executes. This means you cannot use exception handling to "rescue" a transaction that would otherwise roll back. If you need transactional behavior tied to your exception handling logic, use @Transactional(rollbackFor = YourException.class) to explicitly declare which exceptions should trigger rollback. Also note that @ExceptionHandler methods themselves are not transactional, so you cannot use them to initiate or commit transactions.

9. How do you test exception handlers in isolation with @WebMvcTest?

@WebMvcTest loads only the web layer (controllers and @ControllerAdvice), making it ideal for testing exception handlers in isolation. You use MockMvc to perform requests and verify responses. A typical pattern is mockMvc.perform(get("/users/999")).andExpect(status().isNotFound()).andExpect(jsonPath("$.errorCode").value("USER_NOT_FOUND")). You can also use @MockBean to mock service layer dependencies and configure them to throw specific exceptions, then verify your handler produces the expected response. This approach tests the full request-to-response flow including your exception handler, without starting the entire application context.

10. How do you internationalize error messages in Spring exception handling?

Inject MessageSource into your @ControllerAdvice and use it to resolve messages by code. For validation errors, Spring's FieldError already carries the error code from your validation annotation, which you can look up through messageSource.getMessage() with the appropriate locale. For business exceptions, define message codes in your messages.properties file matching your exception class names or custom codes. Pass the HttpServletRequest to your handler method to extract the Accept-Language header and resolve the correct locale. This approach lets you return errors in the client's language without changing your handler logic.

11. What is SimpleMappingExceptionResolver and when would you use it over @ExceptionHandler?

SimpleMappingExceptionResolver is an older Spring exception handling mechanism that maps exception types to view names (JSPs or templates) for rendered HTML responses. It is configured as a bean and applies globally without annotations. The key difference from @ExceptionHandler is that it is view-based rather than API-based — it is designed for server-side rendered applications where you want to forward to an error page. For REST APIs returning JSON, @ExceptionHandler and @ControllerAdvice are superior because they give you full control over the response body. Use SimpleMappingExceptionResolver only when you have a legacy JSP-based application that needs exception-to-page mapping.

12. How does exception handling differ in Spring WebFlux compared to Spring MVC?

WebFlux does not use the same exception handling model as MVC because it is built on reactive streams and non-blocking I/O. Instead of @ExceptionHandler, WebFlux uses WebExceptionHandler (part of the HandlerExceptionHandler interface). You implement WebExceptionHandler and register it as a bean. The handling happens on the reactive pipeline, so you return Mono<ServerResponse> instead of ResponseEntity. Another key difference is that @ControllerAdvice works differently in WebFlux — the reactive equivalent is @ControllerAdvice with @ExceptionHandler methods that return Mono or Flux. Understanding this distinction is critical when migrating exception handling logic between blocking and reactive applications.

13. How do correlation IDs work in distributed exception handling?

A correlation ID is a unique identifier (typically a UUID) attached to every incoming request, propagated through all service calls via the X-Correlation-ID HTTP header. Your API gateway generates it if absent and forwards it to backend services. Each service includes it in every log entry, so when an exception occurs, you can search your logging system by correlation ID and retrieve the entire request chain across all services. In Spring Boot, you typically use a Filter to extract or generate the correlation ID and store it in a ThreadLocal (or MDC for structured logging). Your @ControllerAdvice reads this ID and includes it in the error response, allowing clients to report it when contacting support.

14. How do you design a custom exception hierarchy for a large application?

Define a root application exception (like ApplicationException) extending RuntimeException. Create intermediate exception categories: ResourceNotFoundException for 404s, ValidationException for 400s, AuthorizationException for 403s, and BusinessRuleException for domain logic violations. Each concrete exception carries domain-specific data — for example, UserNotFoundException extends ResourceNotFoundException and carries the user ID. This hierarchy lets your @ControllerAdvice have coarse-grained handlers at the intermediate level while allowing fine-grained handlers for specific exceptions where needed. Avoid the anti-pattern of a single generic exception class, as it makes it impossible to handle different failure modes appropriately.

15. What parameters can @ExceptionHandler methods accept and why?

@ExceptionHandler methods accept several automatically injected parameters: the thrown exception itself (Exception, RuntimeException, or a specific subtype), HttpServletRequest for access to request headers and attributes, WebRequest for attribute access across request scopes, HttpServletResponse for direct response manipulation, Locale for the client's locale, InputStream and OutputStream for raw body access, and Model for view-based applications. Spring resolves these parameters by type, so you can declare only what you need. The exception parameter is always required — it tells Spring which exception type this handler is for. Using fewer parameters keeps handlers focused; resist the temptation to inject everything "just in case."

16. How does exception propagation work through Spring MVC's filter chain?

Exceptions thrown in filters (before the request reaches the controller) do not go through @ExceptionHandler methods. They are caught by Spring's DispatcherServlet error handling, which delegates to registered HandlerExceptionResolver implementations. If a filter sets an attribute on the request with the exception, your @ExceptionHandler can retrieve it via WebRequest.getAttribute("javax.servlet.error.exception", SCOPE_REQUEST). This is why centralized logging and error formatting in filters is separate from @ControllerAdvice — they operate at different layers. For REST APIs, consider using ControllerAdvice to handle both controller exceptions and filter-level errors by implementing ErrorController or by registering a custom HandlerExceptionResolver that stores exceptions for later retrieval.

17. When would you implement ErrorController instead of customizing ErrorAttributes?

Implement a custom ErrorController when you need full control over the error rendering logic — including the ability to serve different error formats based on content negotiation, redirect to error pages in certain scenarios, or completely replace the default error rendering pipeline. ErrorAttributes customization is sufficient for most cases because it lets you modify the error response body while leaving the routing and controller logic to Spring. The key distinction is that ErrorController controls the "what happens after an error occurs" pipeline, while ErrorAttributes only controls "what fields go into the error response." If your customization needs are limited to changing field names or adding correlation IDs, ErrorAttributes is the right tool. If you need to change the HTTP status, add headers, or serve different content types, you need ErrorController.

18. What is the role of ErrorMvcAutoConfiguration in Spring Boot's default error handling?

ErrorMvcAutoConfiguration is the Spring Boot auto-configuration class that sets up the default error handling machinery. It contributes a BasicErrorController (the fallback error handler), a DefaultErrorAttributes (which builds the error attribute map), a DefaultExceptionHandlerResolver, and error page configuration. When you add @ControllerAdvice beans, they take precedence over BasicErrorController for matching exceptions. One counterintuitive behavior: ErrorMvcAutoConfiguration is disabled when you add a bean of type ErrorController, not when you add ErrorAttributes. Understanding this precedence chain helps you debug why an unexpected error page appears instead of your custom JSON error response.

19. How do you handle exceptions that originate from asynchronous components like @Async methods?

Exceptions thrown inside @Async methods are not propagated to the calling thread's exception handling infrastructure. Instead, they are caught by Spring's AsyncUncaughtExceptionHandler, which you can customize by implementing that interface and declaring it as a bean. The default handler logs the exception but produces no client-facing error response. For REST APIs, the challenge is that the async thread that threw the exception has no access to the HTTP response. The recommended pattern is to have your @Async method store the exception result in a result-holding object (like a CompletableFuture with an exception) that the calling controller can inspect and convert into an appropriate error response. This separates the async work's error tracking from the API's error response construction.

20. What are the trade-offs between centralized global exception handling and local per-controller handling?

Global exception handling via @ControllerAdvice provides consistency — all clients receive errors in the same format, making your API predictable and easier to consume. It centralizes logging, correlation ID injection, and security filtering for errors in one place. The downside is reduced visibility: when something breaks, you have to trace from the controller to the handler, and in large applications @ControllerAdvice classes can become large and difficult to maintain. Local @ExceptionHandler methods are visible immediately when reading the controller, making them easier to understand in isolation. They are appropriate when different controllers genuinely need different error formats. The practical compromise is to use @ControllerAdvice for truly global concerns (logging, correlation IDs, security sanitization) while allowing controller-specific handlers for endpoint-specific error nuances.

Further Reading

These deep dives cover advanced topics that build on the exception handling foundation covered above.

  • Validation Exception Handling — MethodArgumentNotValidException and ConstraintViolationException have different sources (body vs parameter validation). Understanding which validator fires when helps you write precise handlers for each case.
  • Exception Handling in Reactive Streams — WebFlux uses a different exception handling model than MVC. WebExceptionHandler replaces @ExceptionHandler in reactive applications, and the propagation semantics are different.
  • Integration with API Gateways — When your Spring Boot service sits behind a gateway like Spring Cloud Gateway or Zuul, error handling crosses process boundaries. The gateway may transform your error response, so design for it.
  • RFC 7807 Problem Details — This standard defines a structured error response format (type, title, status, detail, instance). Spring supports it natively with ProblemDetail and ErrorResponse.
  • Internationalization of Error Messages — Use Spring’s MessageSource to return localized error messages. Users in different locales should see errors in their language, not just English.
  • Exception Handling and Transaction Rollback — By default, @ExceptionHandler methods do not trigger transaction rollback. If your handler runs within a transaction and you need rollback behavior, use @Transactional(rollbackFor = …).
  • Testing Exception Handlers — Use @WebMvcTest with MockMvc to test handlers in isolation. MockMvc.perform(get(“/users/1”)).andExpect(status().isNotFound()) verifies the handler response without spinning up the full application context.
  • Centralized Logging with Correlation IDs — Propagate a X-Correlation-ID header from API Gateway through to your logs. Every exception log should include this ID so you can trace a request across service boundaries.

Conclusion

Spring Boot’s exception handling toolkit gives you everything you need to build APIs that fail gracefully. The key layers are @ExceptionHandler for per-controller cases, @ControllerAdvice for global consistency, ErrorAttributes for response structure, and @ResponseStatus for declarative status codes on exception classes. Combine these tools with proper logging, correlation IDs, and a security-first mindset where stack traces and internal paths never reach clients.

A well-designed exception handling strategy means your API always returns predictable, documented error responses — regardless of what goes wrong internally. That predictability is what separates production-grade APIs from prototypes.

Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path