Global Exception Handling in Spring Boot with @ControllerAdvice

When building RESTful APIs with Spring Boot, exception handling becomes critical for providing meaningful responses and avoiding verbose stack traces in the client response. Rather than handling exceptions in every controller, Spring Boot offers a clean, centralized way: @ControllerAdvice.

In this guide, we’ll explore:

  • What is @ControllerAdvice
  • Handling exceptions globally
  • Returning structured JSON responses
  • Customizing error messages
  • Real-world patterns
Global Exception Handling in Spring Boot

🔍 What is @ControllerAdvice?

@ControllerAdvice is a specialization of @Component used to define global exception handling logic across the entire application. You can catch, log, and respond to exceptions thrown in your controllers—all in one place.

✅ Setup Spring Boot Project

Ensure your project has the following dependency:



  org.springframework.boot
  spring-boot-starter-web


Spring Boot’s web starter already configures Jackson and basic exception handling infrastructure.

💥 Example: Throwing an Exception

Let’s create a simple REST controller that might throw an exception:

📂 Controller: UserController.java


package com.kscodes.springboot.exceptiondemo.controller;

import org.springframework.web.bind.annotation.*;

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

    @GetMapping("/{id}")
    public String getUser(@PathVariable int id) {
        if (id <= 0) {
            throw new IllegalArgumentException("User ID must be greater than 0");
        }
        return "User with ID " + id;
    }
}

This controller throws IllegalArgumentException for invalid IDs.

🧰 Global Exception Handling with @ControllerAdvice

Create a global handler class:

📁 GlobalExceptionHandler.java


package com.kscodes.springboot.exceptiondemo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex) {
        Map errorBody = new HashMap<>();
        errorBody.put("timestamp", LocalDateTime.now());
        errorBody.put("status", HttpStatus.BAD_REQUEST.value());
        errorBody.put("error", "Bad Request");
        errorBody.put("message", ex.getMessage());
        return new ResponseEntity<>(errorBody, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity> handleGenericException(Exception ex) {
        Map errorBody = new HashMap<>();
        errorBody.put("timestamp", LocalDateTime.now());
        errorBody.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        errorBody.put("error", "Internal Server Error");
        errorBody.put("message", "An unexpected error occurred");
        return new ResponseEntity<>(errorBody, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

🔁 Sample Error Response


{
  "timestamp": "2025-06-28T10:15:30.000",
  "status": 400,
  "error": "Bad Request",
  "message": "User ID must be greater than 0"
}

🧪 Testing the Endpoint



curl http://localhost:8080/api/users/0

Returns HTTP 400 Bad Request with the structured JSON error.

🧩 Validating Input with @Valid + Exception Handling

Let’s add a POST endpoint and handle validation errors.

📁 User.java



public class User {
    @NotBlank(message = "Name is required")
    private String name;

    @Email(message = "Email is invalid")
    private String email;
}

📁 UserController.java


@PostMapping
public String createUser(@Valid @RequestBody User user) {
    return "User created: " + user.getName();
}

➕ Add Handler for Validation Errors


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

🔁 Sample Response


{
  "name": "Name is required",
  "email": "Email is invalid"
}

🧼 Clean Error Model (Optional)

Create a custom error response model:


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

    // Getters/Setters/Constructor
}

Modify Handler


@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) {
    ApiError error = new ApiError(LocalDateTime.now(), 404, ex.getMessage(), request.getDescription(false));
    return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}

🧾 Best Practices

TipDescription
Use specific exception classese.g., UserNotFoundException, InvalidRequestException
Return standard structureHelps front-end developers consume error responses consistently
Avoid exposing internalsNever return full stack trace or DB error directly
Log the errorsUse SLF4J/Logback to log details for debugging
Use @ResponseStatus for simple use casesOn custom exceptions if JSON body is not needed

📚 External References

✅ Conclusion

Using @ControllerAdvice, you can manage exceptions in a centralized and consistent way, improving API design and maintainability. With structured responses and specific handlers, your applications become more robust and developer-friendly.