Table of contents
1.
Introduction
2.
Initial Setup
2.1.
Step 1: Creating a JPA Entity Class Customer
2.2.
Step 2: Creating a CustomerRepository Interface
2.3.
Step 3: Creating Custom Exceptions
2.4.
Step 4: Creating the Service Layer
3.
Define Error Response Message  
4.
Create Controller Advice with @ExceptionHandler  
5.
Handling Exceptions
5.1.
Example: GlobalExceptionHandler
6.
Frequently Asked Questions
6.1.
What is the purpose of @ControllerAdvice in Spring Boot?
6.2.
How do custom exceptions improve code clarity?
6.3.
What happens if no exception handler is defined?
7.
Conclusion
Last Updated: Jan 27, 2025
Easy

Global Exception Handling in Spring Boot

Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Global Exception Handling in Spring Boot is a mechanism to manage exceptions across the entire application in a centralized and consistent manner. It helps developers handle errors gracefully, providing clear responses to clients instead of exposing technical details. By implementing global exception handling, you can ensure that all exceptions are processed uniformly, improving application reliability and user experience.

Global Exception Handling in Spring Boot

In this article, we will learn about global exception handling in Spring Boot, its importance, and how to implement it to manage errors efficiently.

Initial Setup

Before diving into exception handling, let’s set up a basic Spring Boot project.

1. Create a Spring Boot Project:

Use Spring Initializr to generate a project with dependencies like Spring Web, Spring Data JPA, and H2 Database.
 

2. Configure application.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect


This configuration sets up an in-memory H2 database for testing.

Step 1: Creating a JPA Entity Class Customer

A Customer entity represents a record in the database.

package com.example.demo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;

    // Constructors
    public Customer() {}

    public Customer(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Step 2: Creating a CustomerRepository Interface

The CustomerRepository interface handles database operations for the Customer entity.

package com.example.demo.repository;


import com.example.demo.entity.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}

Step 3: Creating Custom Exceptions

Custom exceptions provide specific error messages and context.

Example: CustomerNotFoundException

package com.example.demo.exception;
public class CustomerNotFoundException extends RuntimeException {
    public CustomerNotFoundException(String message) {
        super(message);
    }
}

Step 4: Creating the Service Layer

The service layer contains the business logic of the application.

package com.example.demo.service;
import com.example.demo.entity.Customer;
import com.example.demo.exception.CustomerNotFoundException;
import com.example.demo.repository.CustomerRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class CustomerService {

    private final CustomerRepository customerRepository;

    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public List<Customer> getAllCustomers() {
        return customerRepository.findAll();
    }

    public Customer getCustomerById(Long id) {
        return customerRepository.findById(id).orElseThrow(() ->
                new CustomerNotFoundException("Customer with ID " + id + " not found"));
    }

    public Customer addCustomer(Customer customer) {
        return customerRepository.save(customer);
    }
}

Define Error Response Message  

When an error occurs in your application, it’s important to send a clear & meaningful response to the client. This response should include details like the error message, status code, & timestamp. To achieve this, we’ll create a custom error response class.  

This class will act as a template for all error responses. It will contain fields like `message`, `status`, & `timestamp`. Let’s create this step by step.  


Step 1: Create the Error Response Class  

We’ll define a simple Java class to represent the error response. This class will use basic fields & a constructor to initialize them.  

public class ErrorResponse {
    private String message;       // Stores the error message
    private int status;           // Stores the HTTP status code
    private long timestamp;       // Stores the time when the error occurred

   // Constructor to initialize the fields
    public ErrorResponse(String message, int status, long timestamp) {
        this.message = message;
        this.status = status;
        this.timestamp = timestamp;
    }

    // Getters to access the fields
    public String getMessage() {
        return message;
    }

    public int getStatus() {
        return status;
    }

    public long getTimestamp() {
        return timestamp;
    }
}


In this Code: 

1. Fields:  
 

  • `message`: A string that describes the error.  
     
  • `status`: An integer representing the HTTP status code (e.g., 404 for "Not Found").  
     
  • `timestamp`: A long value storing the time when the error occurred.  


2. Constructor:  

  • The constructor initializes the fields with the values passed to it.  


3. Getters:  

  • Getters are used to retrieve the values of the fields. This is important because Spring Boot will use these getters to convert the object into JSON.  


Step 2: Using the Error Response Class  

Once the `ErrorResponse` class is defined, it can be used in your exception-handling logic to send structured error responses. For example, if a user requests a resource that doesn’t exist, you can return an `ErrorResponse` object with a 404 status code & a message like "Resource not found".  

This approach ensures that all error responses follow the same structure, making it easier for clients to understand & handle errors.  

Create Controller Advice with @ExceptionHandler  

In Spring Boot, `@ControllerAdvice` is a powerful annotation that allows you to handle exceptions globally. Instead of writing exception-handling logic in every controller, you can centralize it in one class. This makes your code cleaner & easier to maintain.  

The `@ExceptionHandler` annotation is used inside the `@ControllerAdvice` class to define methods that handle specific exceptions. Let’s create a `GlobalExceptionHandler` class step by step.  

Step 1: Create the GlobalExceptionHandler Class  

We’ll create a class annotated with `@ControllerAdvice`. Inside this class, we’ll define methods to handle exceptions using `@ExceptionHandler`.  

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;


@ControllerAdvice
public class GlobalExceptionHandler {

    // Handle generic exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
            ex.getMessage(),                          // Error message
            HttpStatus.INTERNAL_SERVER_ERROR.value(), // HTTP status code (500)
            System.currentTimeMillis()               // Timestamp
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // Handle specific exceptions (e.g., ResourceNotFoundException)
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
        ResourceNotFoundException ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
            ex.getMessage(),                          // Error message
            HttpStatus.NOT_FOUND.value(),             // HTTP status code (404)
            System.currentTimeMillis()               // Timestamp
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }
}


Explanation of the Code  

1. `@ControllerAdvice`: 

This annotation tells Spring Boot that this class will handle exceptions globally for all controllers.  

 

2. `@ExceptionHandler`:  

This annotation is used to define methods that handle specific exceptions. For example, `@ExceptionHandler(Exception.class)` handles all generic exceptions, while `@ExceptionHandler(ResourceNotFoundException.class)` handles a custom exception (we’ll define this later).  
 

3. `ResponseEntity<ErrorResponse>`:  

This is the return type of the exception-handling methods. It allows us to send a custom error response along with an HTTP status code.  
 

4. `ErrorResponse`:  

We use the `ErrorResponse` class (created earlier) to structure the error response.  
 

Step 2: Define a Custom Exception (Optional)  

To demonstrate handling specific exceptions, let’s create a custom exception called `ResourceNotFoundException`.  

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}


This exception can be thrown when a requested resource is not found. For example, if a user tries to access a record that doesn’t exist in the database, you can throw this exception.  


Step 3: How It Works  

  • When an exception occurs in any controller, Spring Boot will look for a matching `@ExceptionHandler` method in the `GlobalExceptionHandler` class.  
     
  • If a matching method is found, it will execute that method & return the custom error response to the client.  
     
  • If no matching method is found, the generic `handleGlobalException` method will handle the exception.  

Modify Controller for using @ControllerAdvice  

Now that we’ve created the `GlobalExceptionHandler` class, we need to modify our controllers to throw exceptions that will be handled by this global handler. This ensures that all exceptions are managed in one place, making the code cleaner & more maintainable.  

Let’s create a simple controller & demonstrate how to use the `@ControllerAdvice` class.  


Step 1: Create a Controller  

We’ll create a `UserController` class that handles HTTP requests related to users. This controller will throw exceptions in certain scenarios, which will be caught by the `GlobalExceptionHandler`.  

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    // Simulate a method to fetch user by ID
    @GetMapping("/{id}")
    public String getUserById(@PathVariable int id) {
        // Simulate a scenario where the user is not found
        if (id != 1) {
            throw new ResourceNotFoundException("User not found with ID: " + id);
        }
        return "User found with ID: " + id;
    }

    // Simulate a method that throws a generic exception
    @GetMapping("/error")
    public String triggerError() {
        throw new RuntimeException("An unexpected error occurred!");
    }
}


In this Code: 

1. `@RestController`:  

This annotation marks the class as a controller where every method returns a domain object instead of a view.  
 

2. `@RequestMapping("/users")`:  

This annotation maps all methods in the controller to the `/users` URL path.  
 

3. `@GetMapping("/{id}")`:  

This method handles GET requests to fetch a user by their ID. If the ID is not 1, it throws a `ResourceNotFoundException`.  
 

4. `@GetMapping("/error")`:  

This method simulates a scenario where a generic exception is thrown.  


Step 2: How It Works with @ControllerAdvice  

  • When a request is made to `/users/{id}`, the `getUserById` method is called.  
    - If the ID is not 1, it throws a `ResourceNotFoundException`.  

    - This exception is caught by the `handleResourceNotFoundException` method in the `GlobalExceptionHandler` class.  

    - A custom error response is returned to the client with a 404 status code.  
     
  • When a request is made to `/users/error`, the `triggerError` method is called.  
    - It throws a `RuntimeException`.  

    - This exception is caught by the `handleGlobalException` method in the `GlobalExceptionHandler` class.  

    - A custom error response is returned to the client with a 500 status code.  


Step 3: Testing the Controller  

You can test the controller using tools like Postman or directly in your browser.  


1. Valid Request:  

URL: `http://localhost:8080/users/1`  
Response: `"User found with ID: 1"`  


2. Invalid Request (User Not Found):  

URL: `http://localhost:8080/users/2`  
Response:  
     {
       "message": "User not found with ID: 2",
       "status": 404,
       "timestamp": 1698765432100
     }


3. Generic Error:  

URL: `http://localhost:8080/users/error`  

Response:  
     {
       "message": "An unexpected error occurred!",
       "status": 500,
       "timestamp": 1698765432200
     }

Handling Exceptions

Global exception handling centralizes the error-handling logic. In Spring Boot, we use the @ControllerAdvice annotation.

Example: GlobalExceptionHandler

package com.example.demo.handler;
import com.example.demo.exception.CustomerNotFoundException;
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 java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;


@ControllerAdvice
public class GlobalExceptionHandler {


    @ExceptionHandler(CustomerNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleCustomerNotFoundException(CustomerNotFoundException ex) {
        Map<String, Object> response = new HashMap<>();
        response.put("timestamp", LocalDateTime.now());
        response.put("message", ex.getMessage());
        response.put("status", HttpStatus.NOT_FOUND.value());


        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }


    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleGeneralException(Exception ex) {
        Map<String, Object> response = new HashMap<>();
        response.put("timestamp", LocalDateTime.now());
        response.put("message", "An unexpected error occurred");
        response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());


        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}


Explanation of the Code:

  1. @ControllerAdvice: Makes this class a global exception handler.
     
  2. @ExceptionHandler: Specifies the type of exception to handle.
     
  3. Response Structure: Includes a timestamp, message, and HTTP status code.
     

Output:

When a CustomerNotFoundException is thrown, the API will return:

{
    "timestamp": "2025-01-24T10:00:00",
    "message": "Customer with ID 1 not found",
    "status": 404
}

Frequently Asked Questions

What is the purpose of @ControllerAdvice in Spring Boot?

@ControllerAdvice is used to handle exceptions globally across the entire application, ensuring a consistent error response structure.

How do custom exceptions improve code clarity?

Custom exceptions provide specific context and error messages, making debugging and maintenance easier.

What happens if no exception handler is defined?

If no exception handler is defined, Spring Boot’s default error response will be returned, which may not be user-friendly.

Conclusion

In this article, we covered how to implement global exception handling in a Spring Boot application. We set up a basic project, created a JPA entity, repository, custom exceptions, and a service layer, and centralized error handling using @ControllerAdvice. 

Live masterclass