404 Not Found

404 Not Found


nginx

Global Exception Handling

Global exception handling standardizes the format of API error responses—clients no longer have to deal with a wide variety of error formats, making debugging and integration more efficient.

1. What You'll Learn


2. A True Story of a Front-End Developer

(1) Pain Point: Inconsistent error response formats

Bob is a front-end developer who ran into issues while integrating with the OrderFlow API: some endpoints returned a 404 with plain text "Not Found", some returned a 500 with an HTML error page, some returned a business exception {"error": "xxx"}, and others returned {"message": "xxx"}. He had to write different error-handling logic for each endpoint, resulting in try-catch blocks scattered throughout his code—and he often overlooked them, causing the page to go blank.

(2) Solution for @RestControllerAdvice

A unified exception handler ensures that all error responses follow a consistent format:

JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage(), Instant.now()));
    }
}

(3) Revenue

After Alice implemented global exception handling, all error response formats were standardized to {code, message, timestamp, details}, and Bob's front end only needed a single, unified error-handling function, resulting in a fivefold increase in integration efficiency.


3. The @RestControllerAdvice Mechanism

(1) Exception Handling Dispatch Process

100%
graph TD
    A[Controller throws Exception] --> B{Spring DispatcherServlet}
    B --> C["@RestControllerAdvice<br/>Scans @ExceptionHandler"]
    C --> D{Match Exception Type?}
    D -->|Yes| E["Execute @ExceptionHandler<br/>Return ErrorResponse"]
    D -->|No| F["Spring Default<br/>Error Response"]
    E --> G["Client receives<br/>Consistent JSON"]
    F --> H["Client receives<br/>Inconsistent response"]
Note Purpose Location
@RestControllerAdvice Global Exception Handling Class On the class
@ExceptionHandler Handling Specified Exception Types On the Method
@ResponseStatus Specify a response status code on an exception class or method

(1) ▶ Example: Basic Global Exception Handler

JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "RESOURCE_NOT_FOUND", ex.getMessage(), Instant.now(), null);
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(
            BusinessException ex) {
        ErrorResponse error = new ErrorResponse(
            "BUSINESS_ERROR", ex.getMessage(), Instant.now(), null);
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_ERROR", "An unexpected error occurred", Instant.now(), null);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Output:

TEXT
// Execution Successful

4. Custom Business Exception System

(1) Exception Class Hierarchy

100%
graph TD
    A[RuntimeException] --> B[BusinessException<br/>Base business exception]
    B --> C[ResourceNotFoundException<br/>404 Not Found]
    B --> D[InsufficientStockException<br/>422 Business Rule Violation]
    B --> E[OrderStateException<br/>422 Invalid State Transition]
    A --> F[ValidationException<br/>400 Bad Request]

(1) ▶ Example: Custom Exception Class

JAVA
// Base business exception
public class BusinessException extends RuntimeException {
    private final String errorCode;

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

// Resource not found
public class ResourceNotFoundException extends BusinessException {
    public ResourceNotFoundException(String resource, Long id) {
        super("RESOURCE_NOT_FOUND",
            resource + " not found with id: " + id);
    }
}

// Insufficient stock
public class InsufficientStockException extends BusinessException {
    public InsufficientStockException(Long productId, int available, int requested) {
        super("INSUFFICIENT_STOCK",
            String.format("Product %d: available=%d, requested=%d",
                productId, available, requested));
    }
}

// Invalid order state
public class OrderStateException extends BusinessException {
    public OrderStateException(Long orderId, String current, String target) {
        super("INVALID_ORDER_STATE",
            String.format("Order %d: cannot transition from %s to %s",
                orderId, current, target));
    }
}

Output:

TEXT
// Execution Successful

5. Error Response DTO Design

(1) ErrorResponse Design Principles

(1) ▶ Examples: ErrorResponse and ValidationErrorResponse

JAVA
public record ErrorResponse(
    String code,
    String message,
    Instant timestamp,
    String traceId,
    List<FieldError> details
) {
    public record FieldError(
        String field,
        String message,
        Object rejectedValue
    ) {}
}

// Convenience factory methods
public class ErrorResponse {
    public static ErrorResponse of(String code, String message, String traceId) {
        return new ErrorResponse(code, message, Instant.now(), traceId, null);
    }

    public static ErrorResponse withDetails(String code, String message,
            String traceId, List<FieldError> details) {
        return new ErrorResponse(code, message, Instant.now(), traceId, details);
    }
}

Output:

TEXT
// Execution Successful
Field Type Description
code String Error code (machine-readable)
message String Error message (human-readable)
timestamp Instant Time of Occurrence
traceId String Trace ID (Associated Log)
details List Field-level error details (for validation errors)

6. Special Handling of Validation Errors

(1) ▶ Example: Handling MethodArgumentNotValidException

JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();

        List<ErrorResponse.FieldError> details = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(fe -> new ErrorResponse.FieldError(
                fe.getField(),
                fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
                fe.getRejectedValue()
            ))
            .toList();

        ErrorResponse error = ErrorResponse.withDetails(
            "VALIDATION_ERROR",
            "Input validation failed",
            traceId,
            details
        );

        log.warn("Validation failed [traceId={}]: {}", traceId, details);
        return ResponseEntity.badRequest().body(error);
    }
}

Output:

TEXT
// Execution Successful
💻 Output:

JSON
{
  "code": "VALIDATION_ERROR",
  "message": "Input validation failed",
  "timestamp": "2024-01-15T10:00:00Z",
  "traceId": "abc-123-def",
  "details": [
    {"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": 0},
    {"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
  ]
}

7. Exception Logs and Trace IDs

(1) ▶ Example: A complete exception handler with a tracking ID

JAVA
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ResourceNotFoundException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.warn("Resource not found [traceId={}]: {}", traceId, ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
    }

    @ExceptionHandler(InsufficientStockException.class)
    public ResponseEntity<ErrorResponse> handleInsufficientStock(
            InsufficientStockException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.warn("Insufficient stock [traceId={}]: {}", traceId, ex.getMessage());
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(
            BusinessException ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.warn("Business error [traceId={}]: {}", traceId, ex.getMessage());
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(
            Exception ex,
            HttpServletRequest request) {
        String traceId = generateTraceId();
        log.error("Unexpected error [traceId={}]", traceId, ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.of("INTERNAL_ERROR",
                "An unexpected error occurred. TraceId: " + traceId, traceId));
    }

    private String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
}

Output:

TEXT
// Execution Successful
Error Type HTTP Status Code Log Level Description
ResourceNotFoundException 404 WARN Resource does not exist; client issue
InsufficientStockException 409 WARN Business Conflict
BusinessException 422 WARN Business rule violation
MethodArgumentNotValidException 400 WARN Input validation failed
Exception 500 ERROR Unexpected error; needs to be investigated

8. Comprehensive Example: OrderFlow's Complete Exception Handling System

JAVA
// ErrorResponse.java
package com.orderflow.exception;

import java.time.Instant;
import java.util.List;

public record ErrorResponse(
    String code, String message, Instant timestamp,
    String traceId, List<FieldError> details
) {
    public record FieldError(String field, String message, Object rejectedValue) {}

    public static ErrorResponse of(String code, String message, String traceId) {
        return new ErrorResponse(code, message, Instant.now(), traceId, null);
    }

    public static ErrorResponse withDetails(String code, String message,
            String traceId, List<FieldError> details) {
        return new ErrorResponse(code, message, Instant.now(), traceId, details);
    }
}

// BusinessException hierarchy
public class BusinessException extends RuntimeException {
    private final String errorCode;
    public BusinessException(String errorCode, String message) {
        super(message); this.errorCode = errorCode;
    }
    public String getErrorCode() { return errorCode; }
}

public class ResourceNotFoundException extends BusinessException {
    public ResourceNotFoundException(String resource, Long id) {
        super("RESOURCE_NOT_FOUND", resource + " not found with id: " + id);
    }
}

public class InsufficientStockException extends BusinessException {
    public InsufficientStockException(Long productId, int avail, int req) {
        super("INSUFFICIENT_STOCK",
            "Product " + productId + ": available=" + avail + ", requested=" + req);
    }
}

// GlobalExceptionHandler.java
package com.orderflow.exception;

import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        String tid = tid();
        log.warn("[{}] {}", tid, ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
    }

    @ExceptionHandler(InsufficientStockException.class)
    public ResponseEntity<ErrorResponse> handleStock(InsufficientStockException ex) {
        String tid = tid();
        log.warn("[{}] {}", tid, ex.getMessage());
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        String tid = tid();
        List<ErrorResponse.FieldError> details = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(f -> new ErrorResponse.FieldError(f.getField(),
                f.getDefaultMessage() != null ? f.getDefaultMessage() : "", f.getRejectedValue()))
            .toList();
        log.warn("[{}] Validation failed: {}", tid, details);
        return ResponseEntity.badRequest()
            .body(ErrorResponse.withDetails("VALIDATION_ERROR", "Validation failed", tid, details));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        String tid = tid();
        log.error("[{}] Unexpected error", tid, ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.of("INTERNAL_ERROR", "Unexpected error. Ref: " + tid, tid));
    }

    private String tid() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
}

❓ FAQ

Q What is the difference between @ControllerAdvice and @RestControllerAdvice?
A @RestControllerAdvice = @ControllerAdvice + @ResponseBody. In REST API projects, always use @RestControllerAdvice; method return values are automatically serialized to JSON.
Q What is the matching order for @ExceptionHandler?
A Spring selects the most specific exception type match. If handlers for both BusinessException and Exception are registered, the BusinessException handler takes precedence when a BusinessException is thrown.
Q How can multiple @RestControllerAdvice classes coexist?
A You can use @Order to control priority. @Order(Ordered.HIGHEST_PRECEDENCE) takes precedence in matching. Different Advice classes can handle different types of exceptions.
Q Should the production environment return an exception stack trace?
A No, it should not. The production environment should only return error codes and generic messages; it should not expose its internal implementation. Stack trace information is logged only. For unexpected exceptions, return a traceId so that operations can pinpoint the issue through the logs.
Q How do I handle Spring Security exceptions?
A Spring Security exceptions (such as AccessDeniedException) are thrown within the filter chain and do not pass through the @RestControllerAdvice. You need to customize the AuthenticationEntryPoint and AccessDeniedHandler.
Q What is the relationship between traceId and MDC?
A traceId is used to associate logs with the client in the response, while MDC is used within the logging framework to associate all logs from the same request. It is recommended to use the same value for both; set MDC.put("traceId", id) in the Filter.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Implement a GlobalExceptionHandler for OrderFlow to handle ResourceNotFoundException and BusinessException, and return a standardized ErrorResponse format.

  2. Advanced Exercise (Difficulty: ⭐⭐): Add a handler for MethodArgumentNotValidException and extract field-level error details. Implement traceId generation and use it in the log.

  3. Challenge (Difficulty: ⭐⭐⭐): Create a Servlet Filter that generates a traceID at the request entry point and sets it in the MDC, so that all logs automatically include the traceID. The GlobalExceptionHandler reads the traceID from the MDC to enable end-to-end log tracing.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏