DEV Community

DevCorner2
DevCorner2

Posted on

๐Ÿ›ก๏ธ Mastering Global Exception Handling in Spring Boot (Production-Grade)

Label: Spring Boot, Production Ready, Exception Handling, REST API, Clean Code


๐Ÿ“Œ Introduction

Robust exception handling is the cornerstone of any production-grade REST API. In Spring Boot, we aim to:

  • Avoid duplicated try-catch blocks
  • Standardize error responses
  • Handle custom and system exceptions cleanly
  • Keep logs clean for diagnostics

This blog will walk you through setting up a clean and reusable Global Exception Handler in Spring Boot.


๐ŸŽฏ Goals

  • Create centralized exception handling using @ControllerAdvice
  • Send consistent error responses
  • Support custom exceptions like ResourceNotFoundException
  • Handle validation and internal server errors

๐Ÿงฑ Folder Structure (Recommended)

src/main/java/com/example/demo
โ”œโ”€โ”€ controller
โ”‚   โ””โ”€โ”€ ProductController.java
โ”œโ”€โ”€ dto
โ”‚   โ””โ”€โ”€ ErrorResponse.java
โ”œโ”€โ”€ exception
โ”‚   โ”œโ”€โ”€ GlobalExceptionHandler.java
โ”‚   โ”œโ”€โ”€ ResourceNotFoundException.java
โ”‚   โ””โ”€โ”€ ValidationException.java (optional)
โ””โ”€โ”€ DemoApplication.java
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ง Step 1: Create Custom Exception Classes

โœ… ResourceNotFoundException

package com.example.demo.exception;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”น Use one exception per business rule for clarity.


๐Ÿงพ Step 2: Standardized Error Response DTO

package com.example.demo.dto;

import java.time.LocalDateTime;

public class ErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;

    public ErrorResponse(int status, String error, String message, String path) {
        this.timestamp = LocalDateTime.now();
        this.status = status;
        this.error = error;
        this.message = message;
        this.path = path;
    }

    // Getters and Setters
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”น Always provide timestamp, status, and path for better traceability.


๐ŸŒ Step 3: Global Exception Handler (@ControllerAdvice)

package com.example.demo.exception;

import com.example.demo.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
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.stream.Collectors;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                HttpStatus.NOT_FOUND.getReasonPhrase(),
                ex.getMessage(),
                request.getRequestURI()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) {
        String errorMsg = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .collect(Collectors.joining(", "));

        ErrorResponse error = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                "Validation Failed",
                errorMsg,
                request.getRequestURI()
        );
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex, HttpServletRequest request) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
                ex.getMessage(),
                request.getRequestURI()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
Enter fullscreen mode Exit fullscreen mode

๐ŸŸจ Pro Tip: Always add a fallback @ExceptionHandler(Exception.class) to handle uncaught runtime exceptions.


๐Ÿ” Step 4: Sample Controller

package com.example.demo.controller;

import com.example.demo.exception.ResourceNotFoundException;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/products")
@Validated
public class ProductController {

    @GetMapping("/{id}")
    public String getProductById(@PathVariable @Min(1) int id) {
        if (id != 1) {
            throw new ResourceNotFoundException("Product with ID " + id + " not found.");
        }
        return "Product ID: " + id;
    }
}
Enter fullscreen mode Exit fullscreen mode

โœ… You can now test /products/99 and see the formatted JSON error.


๐Ÿ“ค Sample Error Response

๐Ÿšซ /products/999

{
  "timestamp": "2025-06-16T10:25:48.709",
  "status": 404,
  "error": "Not Found",
  "message": "Product with ID 999 not found.",
  "path": "/products/999"
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿšซ /products/-1

{
  "timestamp": "2025-06-16T10:26:14.109",
  "status": 400,
  "error": "Validation Failed",
  "message": "id: must be greater than or equal to 1",
  "path": "/products/-1"
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงผ Logging (Optional but Recommended)

For production, log exceptions using a logging framework (e.g., SLF4J + Logback):

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

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex, HttpServletRequest request) {
    logger.error("Unhandled exception occurred", ex);
    ...
}
Enter fullscreen mode Exit fullscreen mode

โœ… Best Practices Checklist

  • [x] Use @ControllerAdvice to centralize error handling.
  • [x] Create reusable, specific custom exceptions.
  • [x] Never leak stack traces to clients.
  • [x] Use proper HTTP codes: 404 for missing data, 400 for validation, 500 for generic.
  • [x] Always include request URI and timestamp in response.

๐Ÿ“ฆ Bonus: Extend with Custom Exception Interface

If you want even cleaner control:

public interface ApiException {
    HttpStatus getStatus();
    String getErrorCode();  // optional for localization or tracking
}
Enter fullscreen mode Exit fullscreen mode

Your custom exception can implement this for dynamic mapping.


๐Ÿ“Ž Final Thoughts

Global exception handling isn't just about catching errors โ€” it's about designing fault-tolerant, debuggable, and user-friendly APIs. This pattern is a must-have in any Spring Boot project going into production.


Top comments (0)