Spring Boot Exception Handling

Exception handling controls what your API sends back when something goes wrong. Without proper handling, Spring exposes raw Java stack traces to the client — a security risk and a poor experience. Structured error handling returns clean, useful JSON responses.

The Default Spring Error Response

Without any configuration, Spring returns this when an exception occurs:

{
  "timestamp": "2024-05-01T10:00:00.000+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/api/users/999"
}

This is generic and exposes little detail. You want something like this instead:

{
  "status": 404,
  "message": "User with ID 999 not found",
  "timestamp": "2024-05-01T10:00:00"
}

Step 1 — Create a Custom Exception

public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(Long id) {
        super("User with ID " + id + " not found");
    }
}

Step 2 — Create an Error Response DTO

public class ErrorResponse {
    private int status;
    private String message;
    private String timestamp;

    // Constructor, getters
}

Step 3 — Create a Global Exception Handler

@ControllerAdvice intercepts exceptions thrown from any controller in your app:

@RestControllerAdvice                         ← Applies to all controllers
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(
        UserNotFoundException ex
    ) {
        ErrorResponse error = new ErrorResponse(
            404,
            ex.getMessage(),
            LocalDateTime.now().toString()
        );
        return ResponseEntity.status(404).body(error);
    }

    @ExceptionHandler(Exception.class)        ← Catch-all for unexpected errors
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            500,
            "An unexpected error occurred",
            LocalDateTime.now().toString()
        );
        return ResponseEntity.status(500).body(error);
    }
}

How Exception Flow Works

  Client sends GET /users/999
         │
         ▼
  UserController.getUserById(999)
         │
         ▼
  UserService.findById(999)
         │
         ▼
  User not found → throw new UserNotFoundException(999)
         │
         ▼ (Spring intercepts)
  GlobalExceptionHandler.handleUserNotFound(ex)
         │
         ▼
  Returns: HTTP 404 with ErrorResponse JSON
         │
         ▼
  Client receives clean error message

@ExceptionHandler on a Single Controller

Handle exceptions only within one controller by placing the method inside that controller (no @ControllerAdvice needed):

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

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<String> handleNotFound(ProductNotFoundException ex) {
        return ResponseEntity.status(404).body(ex.getMessage());
    }
}

Handling Validation Errors

When a request body fails validation (covered in the Validation topic), Spring throws MethodArgumentNotValidException:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
    MethodArgumentNotValidException ex
) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error ->
        errors.put(error.getField(), error.getDefaultMessage())
    );
    return ResponseEntity.badRequest().body(errors);
}

Response example:

{
  "email": "must be a valid email address",
  "name": "must not be blank"
}

Exception Handling Hierarchy

Priority (highest to lowest)
────────────────────────────────────────────
@ExceptionHandler inside the controller     ← Most specific
@ExceptionHandler in @ControllerAdvice      ← Application-wide
Default Spring error handling               ← Fallback

Summary

  • Use @RestControllerAdvice with @ExceptionHandler to handle errors globally
  • Create custom exception classes with meaningful messages
  • Return a structured ErrorResponse DTO instead of raw stack traces
  • Always include a catch-all handler for unexpected Exception types
  • Validation errors throw MethodArgumentNotValidException — handle it separately

Leave a Comment

Your email address will not be published. Required fields are marked *