Welcome to our next lesson in building REST APIs with Spring Boot. In our previous session, you learned how to use ResponseEntity to gain full control over your API's responses, meticulously crafting status codes, headers, and the response body. This was a significant step toward building professional and predictable APIs.
However, so far we have focused on the "happy path" and manually handling known error states, like a resource not being found. What happens when an unexpected error occurs, or when the same type of error can happen in many different places? In this lesson, we will build upon your knowledge of ResponseEntity to create a robust, centralized exception handling mechanism. You will learn how to use the @ControllerAdvice and @ExceptionHandler annotations to intercept exceptions across your entire application and convert them into clean, consistent, and informative JSON error responses. This will keep your controller code clean and make your API much more reliable and easier for clients to use.
The Problem: Scattered and Repetitive Error Handling
Imagine your application grows. You might have ten, twenty, or even a hundred controller methods. Many of them could fail in similar ways, for example, by not finding a requested resource. The naive approach is to wrap the logic in each method with a try-catch block.
This video demonstrates exactly why that approach is problematic. It starts by showing how try-catch blocks quickly lead to duplicated code, making the application difficult to maintain.
Master Exception Handling in Spring Boot: @ExceptionHandler & @ControllerAdvice Explained
This video from the CodeSnippet channel clearly illustrates the evolution from poor to best practices in exception handling.
Watch these three segments to understand the problem we're solving: The traditional approach: The presenter implements try-catch logic directly in a controller method. A better way: He then refactors to use @ExceptionHandler within the same controller, which centralizes the logic for that single controller but doesn't solve the bigger problem. The global problem: Finally, he introduces a second controller to show how the @ExceptionHandler logic must be duplicated, highlighting the need for a truly global solution.
As you saw, duplicating error-handling logic violates the "Don't Repeat Yourself" (DRY) principle and creates a maintenance nightmare. A change to the error response format would require updates in every single catch block. Spring provides a much more elegant solution.
A Centralized Solution: @ControllerAdvice and @ExceptionHandler
Spring allows us to create a global exception handler using a combination of two powerful annotations:
@ControllerAdvice: This annotation marks a class as a global configuration component. It can intercept exceptions thrown by any@RestControllerin your application. For REST APIs, the specialized@RestControllerAdviceis often used, which combines@ControllerAdviceand@ResponseBody.@ExceptionHandler: This annotation is placed on a method within a@ControllerAdviceclass. It specifies which exception (or exceptions) the method is responsible for handling.
When an exception is thrown from a controller, Spring will look for an @ExceptionHandler method in a @ControllerAdvice class that can handle that specific exception type. This decouples exception handling from your business logic, keeping your controllers clean and focused.
This flowchart illustrates the decision process Spring follows:

Let's walk through the steps to implement this pattern.
Step 1: Define a Custom Exception and Error Message Structure
First, it's a best practice to create your own custom exceptions and a standardized structure for your error responses. This makes your code more expressive and your API's error format predictable for clients.
We will use the excellent guide from bezkoder.com to structure our implementation.
Spring Boot @ControllerAdvice & @ExceptionHandler ...
This article provides a clear, step-by-step guide to building a global exception handler.
First, read the sections on defining a custom error response and creating a custom exception. Focus on the ErrorMessage class. This is a simple Plain Old Java Object (POJO) that will be serialized to JSON. Next, review the ResourceNotFoundException class. Notice it extends RuntimeException. In Spring, it's common to use unchecked exceptions for such cases to avoid cluttering method signatures with throws clauses.
The project structure for this pattern typically involves creating a new package, often named exception, to hold these new classes.

Step 2: Create the Global Exception Handler Class
Now for the main event. We'll create a class annotated with @RestControllerAdvice that contains the handler methods.
Spring Boot @ControllerAdvice & @ExceptionHandler ...
Let's continue with the bezkoder article to implement the handler itself.
Read the section Create Controller Advice. Pay close attention to the ControllerExceptionHandler class. Note the following: The class is annotated with @ControllerAdvice. The resourceNotFoundException method is annotated with @ExceptionHandler(ResourceNotFoundException.class). This tells Spring to call this method whenever a ResourceNotFoundException is thrown from any controller. The method's logic creates an ErrorMessage object and returns it inside a ResponseEntity, giving us full control over the status code (404 NOT_FOUND). There's also a globalExceptionHandler method for Exception.class. This is a crucial "catch-all" to handle any unexpected errors and return a 500 INTERNAL_SERVER_ERROR, preventing stack traces from leaking to the client.
For REST APIs, it's generally better to use @RestControllerAdvice. This annotation is a convenience that combines @ControllerAdvice and @ResponseBody, ensuring that the return value of your handler methods is automatically serialized into the response body without needing to wrap everything in a ResponseEntity if you just want to return a POJO. However, since returning a ResponseEntity gives us explicit control over the status code, either @ControllerAdvice or @RestControllerAdvice will work perfectly in this pattern.
Step 3: Refactor the Controller to Throw Exceptions
The final step is to clean up our controller. Instead of catching exceptions or manually returning 404 Not Found responses, we will now simply throw our custom exception when a resource isn't found.
Spring Boot @ControllerAdvice & @ExceptionHandler ...
This last section from the bezkoder article shows the payoff of our new setup.
Read the section Modify Controller. Focus on the getTutorialById method. Notice how clean it is. The try-catch block is gone. Instead, it uses Optional.orElseThrow(), a very common and expressive pattern in modern Java, to throw our ResourceNotFoundException if the Optional returned by the repository is empty.
The logic is now simple: the controller's job is to call the service and return data. If something goes wrong, it throws an exception describing the problem. A separate, global component is then responsible for catching that exception and formatting the appropriate HTTP response. This is a great example of the Separation of Concerns principle.
Hands-On Application
It's time to apply this to your project. You will refactor your GroupController to use this global exception handling mechanism.
-
Create an
exceptionpackage: Inside your main package (com.example.demo), create a new package namedexception. -
Create a custom exception: In the
exceptionpackage, create aResourceNotFoundException.javaclass that extendsRuntimeException. It only needs a constructor that accepts a message string and passes it to thesuper()constructor.// in com.example.demo.exception.ResourceNotFoundException.java public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } } -
Create a standard error DTO: In the same package, create an
ApiError.javaclass to define your error structure. This will be yourErrorMessageequivalent.// in com.example.demo.exception.ApiError.java import java.time.LocalDateTime; public class ApiError { private int statusCode; private LocalDateTime timestamp; private String message; private String description; // Constructor, getters... }(You can use your IDE to generate the constructor and getters.)
-
Create the Global Exception Handler: In the
exceptionpackage, createGlobalExceptionHandler.java.// in com.example.demo.exception.GlobalExceptionHandler.java 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 java.time.LocalDateTime; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ApiError> handleResourceNotFoundException( ResourceNotFoundException ex, WebRequest request) { ApiError apiError = new ApiError( HttpStatus.NOT_FOUND.value(), LocalDateTime.now(), ex.getMessage(), request.getDescription(false) // This gets the request URI ); return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND); } // Add a handler for generic exceptions @ExceptionHandler(Exception.class) public ResponseEntity<ApiError> handleGlobalException( Exception ex, WebRequest request) { ApiError apiError = new ApiError( HttpStatus.INTERNAL_SERVER_ERROR.value(), LocalDateTime.now(), "An unexpected error occurred", request.getDescription(false) ); return new ResponseEntity<>(apiError, HttpStatus.INTERNAL_SERVER_ERROR); } } -
Refactor
GroupController:- Open your
GroupController. - In the
getGroupByIdmethod, replace theOptionalhandling logic. It should now look like this:@GetMapping("/{id}") public ResponseEntity<Group> getGroupById(@PathVariable Long id) { Group group = groupRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Group not found with id: " + id)); return ResponseEntity.ok(group); } - Refactor the
updateanddeletemethods similarly, throwing aResourceNotFoundExceptionif the group to be updated or deleted doesn't exist.
- Open your
-
Test with Postman: Run your application. Use Postman to request a group with an ID that you know does not exist (e.g.,
GET http://localhost:8080/api/groups/999). Verify that you receive a404 Not Foundstatus and a JSON body matching the structure of yourApiErrorclass.
Conclusion
Congratulations! You have implemented a clean, centralized, and robust exception handling strategy for your Spring Boot application. This is a fundamental pattern used in virtually all production-grade REST services built with Spring.
Key Takeaways:
- Centralize Logic: Use a class annotated with
@ControllerAdvice(or@RestControllerAdvice) to handle exceptions globally, avoiding code duplication in your controllers. - Specific Handlers: Use methods annotated with
@ExceptionHandlerto create specific handlers for different exception types. - Clean Controllers: Your controllers should focus on business logic and throw descriptive, custom exceptions when things go wrong, rather than handling HTTP responses themselves.
- Consistent Responses: Defining a standard error DTO (like
ApiError) ensures that all error responses from your API have a consistent and predictable structure, which is invaluable for client-side developers. - Catch-All Handler: Always include a general
ExceptionHandler(Exception.class)as a safety net to gracefully handle any unforeseen errors and prevent sensitive information like stack traces from being exposed.
In our next module, we will dive into data persistence with Spring Data JPA. You'll learn how to connect your application to a MySQL database and map your Java objects to database tables. The ResourceNotFoundException you just created will become even more important as we start querying a real database for data that may or may not exist.