Global Exceptions Handling

👉 Implementing @ControllerAdvice, @ExceptionHandler, and Custom Error Responses in Spring Boot for global Exception handling.


Error handling for APIs

Spring Boot provides a powerful and centralized mechanism for handling and customizing exceptions thrown by the application. This is typically achieved using the @ControllerAdvice annotation.

This approach decouples error handling logic from individual controller methods, leading to cleaner and more maintainable code.

The goal is to return a standardized, meaningful error response to the client instead of a raw exception stack trace.


Table of Contents


Use Case

The primary use case of Gloabl Exception handling is to provide a consistent and reliable API contract for error reporting.

When a resource is not found (404), an input is invalid (400), or an internal server error occurs (500), the client should receive a predictable JSON payload containing a timestamp, a clear error message, and request details.


Benefits

Centralized exception handling significantly improves API consistency and developer experience (DX) by eliminates redundant try-catch blocks in controllers, reduces boilerplate code, and ensures that clients always receive well-structured error messages, simplifying debugging and integration.

The use of custom exceptions clearly defines application-specific errors.

Features

  • Centralized Handling:
    • Use @ControllerAdvice to globalize exception handling across all controllers.
  • Exception Mapping:
    • Use @ExceptionHandler to map specific exception classes to handler methods.
  • Status Mapping:
    • Use @ResponseStatus on custom exceptions or set the status explicitly in the ResponseEntity.
  • Standardized Payload:
    • Define a custom ErrorDetails bean to ensure a consistent structure for error responses.
  • Separation of Concerns:
    • Keeps business logic and error handling distinctly separated.

Drawbacks

While beneficial, the setup involves initial overhead by requiring the creation of custom exception classes and the global handler.

Over-reliance on too many highly specific custom exceptions can lead to exception proliferation, making the code harder to navigate.

Moreover, incorrectly configuring the handler can unintentionally expose sensitive internal details.


Maven Dependency

The core functionalities for web applications, including exception handling via @ControllerAdvice and support for ResponseEntity, are provided as part of the spring-boot-starter-web dependency.

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Implementation

ErrorDetails Bean

This class defines the standard structure for the error response payload sent back to the client. It typically includes temporal and context information.

import java.time.LocalDateTime;

public class ErrorDetails {

    private LocalDateTime timestamp;
    private String message;
    private String details;

    // Default Constructor
    public ErrorDetails() {
    }

    // Parametrized Constructor
    public ErrorDetails(LocalDateTime timestamp, String message, String details) {
        super();
        this.timestamp = timestamp;
        this.message = message;
        this.details = details;
    }

    // Constructor, setter-getters... (Assuming these are implemented for brevity)
    // Getter and Setter methods for timestamp, message, and details should be present.
    // Example Getter:
    public LocalDateTime getTimestamp() {
        return timestamp;
    }

    // Example Setter:
    public void setTimestamp(LocalDateTime timestamp) {
        this.timestamp = timestamp;
    }
}

UserNotFoundException

A custom exception class is used to clearly signal an application-specific error.

The @ResponseStatus(code = HttpStatus.NOT_FOUND) annotation is a convenient way to automatically map this exception to a specific HTTP status code, even without a @ControllerAdvice.

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {

    private static final long serialVersionUID = 4882099180124262207L;

    public UserNotFoundException(String message) {
        super(message);
    }
}

CustomizedResponseEntityExceptionHandler

The Global Exception handler monitors the exceptions across applications. The @ControllerAdvice or @RestControllerAdvice is the central component.

It uses @ExceptionHandler methods to intercept and process exceptions across the entire application, allowing us to contruct and return a standardized error response (ResponseEntity<ErrorDetails>) across application.

import java.time.LocalDateTime;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
//@ControllerAdvice(basePackages = "com.srvivek.x.y.z") //for specific package
//@Order(value = 1) // for defining ordering
//@Priority(value = 1) // for defining ordering
//@RestControllerAdvice(basePackages = "com.srvivek.x.y.z") // @ControllerAdvice + @ResponseBody
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(CustomizedResponseEntityExceptionHandler.class);

    /**
    * Generic exception handling (Fallback for all unhandled exceptions).
    * Returns HTTP 500 INTERNAL_SERVER_ERROR.
    * @param ex
    * @param request
    * @return
    * @throws Exception
    */
    @ExceptionHandler(value = Exception.class)
    public final ResponseEntity<ErrorDetails> handleAllException(Exception ex, WebRequest request) throws Exception {

        logger.error("Error stacktrace: {}", ex);

        ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(),
                request.getDescription(false));

        return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    /**
    * Specific exception handling for UserNotFoundException.
    * Returns HTTP 404 NOT_FOUND.
    * @param ex
    * @param request
    * @return
    * @throws Exception
    */
    @ExceptionHandler(value = UserNotFoundException.class)
    public final ResponseEntity<ErrorDetails> handleUserNotFoundException(Exception ex, WebRequest request)
            throws Exception {

        logger.error("Error stacktrace: {}", ex);

        ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(),
                request.getDescription(false));

        return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.NOT_FOUND);
    }
}

Project POC:
Refer the application source code on Github: Exception Handing POC.


Exception Handling Flow

The diagram illustrates how an exception is captured and processed by the global handler before a standardized response is returned to the client.

sequenceDiagram
    participant C as Client
    participant A as Controller
    participant S as Service
    participant E as Exception
    participant H as @ControllerAdvice
    participant L as Logger
    C->>A: API Request (e.g., GET /users/100)
    A->>S: Call Service Method (findUser(100))
    alt User Not Found
        S-->E: Throws UserNotFoundException
        E->>H: Exception Interception
        H->>L: Log Error Stacktrace
        H-->>C: Standardized Error Response (404 NOT FOUND)
    else User Found
        S-->>A: User Object
        A-->>C: HTTP 200 OK Response
    end

Key Observations

FeatureDescriptionExample / Annotation
ScopingAllows the exception handler to apply only to controllers within specific packages, providing modularity.@ControllerAdvice(basePackages = "com.srvivek.x.y.z")
OrderingDefines the precedence when multiple @ControllerAdvice classes exist, ensuring predictable exception resolution.@Order(value = 1) or @Priority(value = 1)
LoggingIt is critical to explicitly log the exception within the handler, as Spring Boot doesn’t automatically log the intercepted exception’s details.logger.error("Error stacktrace: {}", ex);
ConvenienceA specialized annotation that is equivalent to combining @ControllerAdvice and @ResponseBody, streamlining REST API development.@RestControllerAdvice

Best Practices

PracticeDescriptionKey Action / Benefit
Log ExplicitlyAlways ensure the full exception stack trace is captured and recorded on the server side.Use logger.error(ex.getMessage(), ex); in the handler method.
Specific to GenericStructure your @ExceptionHandler methods to catch the most specific exceptions first, with a fallback to the generic Exception.class.Ensures the correct HTTP status and custom message is returned for known errors.
Avoid Exposing DetailsNever include sensitive server-side information (like stack traces or internal paths) in the API error response sent to the client.Sanitize the error message and use a simple details field.
Leverage Spring's Built-inExtend ResponseEntityExceptionHandler to customize handling of common Spring exceptions (e.g., validation failures, method not allowed).Provides easy overrides for exceptions like MethodArgumentNotValidException.
Use Problem Detail StandardAdopt the RFC 7807 standard (type, title, status, detail, instance) for your error response structure.Enhances API maturity, consistency, and client-side error processing.

Security Considerations

  • No Stack Traces in Response: Ensure the client-facing error message never contains server-side stack traces, internal configuration, or sensitive class names.
  • Generic Messages for 5xx: For 500 INTERNAL_SERVER_ERROR or other unexpected errors, provide a generic message like “An unexpected error occurred” and use a unique correlation ID (logged server-side) that the client can reference when reporting the issue.
  • Input Validation: Implement proper input validation before the exception handler to prevent common injection attacks or unexpected behaviour.