Global exception handling means one central class catches the exceptions thrown from every controller in the application and turns them into clean HTTP responses. In Spring Boot you create this class with @RestControllerAdvice, and it holds @ExceptionHandler methods for each kind of error. It is used so that error handling is written once, every API returns errors in the same format, and controllers stay free of try-catch blocks and repeated handlers. Compared with controller-level handling, a new controller is covered automatically without any extra code.
Features
- One
@RestControllerAdviceclass handles errors for all controllers - Same JSON error format everywhere (timestamp, status, error, message, path)
- Handles your custom exceptions and Spring’s built-in exceptions
- A catch-all
Exceptionhandler stops raw stack traces reaching the client - One handler can cover several exception types
- Controllers and services stay clean
Global Exception Handling Types
- @RestControllerAdvice: a global handler that returns JSON directly. This example uses it and it is the normal choice for REST APIs.
- @ControllerAdvice: the older form, which needs
@ResponseBodyon each handler to return JSON. It is mostly used for MVC pages. - Custom exception handlers:
@ExceptionHandlerfor your own exceptions such asStudentNotFoundException(404) andDuplicateStudentException(409). - Built-in exception handlers: handlers for Spring exceptions such as
HttpMessageNotReadableException(broken JSON) andMethodArgumentTypeMismatchException(wrong type in the URL). - Catch-all handler:
@ExceptionHandler(Exception.class)returns a safe 500 response for anything unexpected.
Hands-on Experiment: Creating a spring boot application to understand how common application errors can be handled centrally across all controllers.
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: globalexception
- Name: globalexception
- Package name: com.example.globalexception
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver.
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>student-global-exception-demo</artifactId>
<version>1.0.0</version>
<name>student-global-exception-demo</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 4: application.properties
spring.application.name=student-global-exception-demo
server.port=8082
# MySQL (database is created automatically if missing)
spring.datasource.url=jdbc:mysql://localhost:3306/student_global_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=trueCode language: PHP (php)
Step 5: Create Entity Class
package com.example.globalexception.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private Integer age;
public Student() {
}
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; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
}
package com.example.globalexception.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Integer durationInMonths;
public Course() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public Integer getDurationInMonths() { return durationInMonths; }
public void setDurationInMonths(Integer durationInMonths) { this.durationInMonths = durationInMonths; }
}
Step 6: Create below Repository Interface
package com.example.globalexception.repository;
import com.example.globalexception.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
Optional<Student> findByEmail(String email);
}
package com.example.globalexception.repository;
import com.example.globalexception.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
}
Step 7: Create below Service class
package com.example.globalexception.service;
import com.example.globalexception.entity.Student;
import com.example.globalexception.repository.StudentRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Student createStudent(Student student) {
validate(student);
studentRepository.findByEmail(student.getEmail()).ifPresent(existing -> {
throw new DuplicateStudentException(student.getEmail());
});
student.setId(null);
return studentRepository.save(student);
}
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
public Student getStudentById(Long id) {
return studentRepository.findById(id)
.orElseThrow(() -> new StudentNotFoundException(id));
}
public Student updateStudent(Long id, Student updated) {
Student existing = getStudentById(id);
validate(updated);
studentRepository.findByEmail(updated.getEmail()).ifPresent(other -> {
if (!other.getId().equals(id)) {
throw new DuplicateStudentException(updated.getEmail());
}
});
existing.setName(updated.getName());
existing.setEmail(updated.getEmail());
existing.setAge(updated.getAge());
return studentRepository.save(existing);
}
public void deleteStudent(Long id) {
studentRepository.delete(getStudentById(id));
}
private void validate(Student student) {
if (student.getName() == null || student.getName().isBlank()) {
throw new InvalidDataException("Student name is required");
}
if (student.getEmail() == null || !student.getEmail().contains("@")) {
throw new InvalidDataException("A valid email is required");
}
if (student.getAge() == null || student.getAge() < 15 || student.getAge() > 60) {
throw new InvalidDataException("Age must be between 15 and 60");
}
}
}
package com.example.globalexception.service;
import com.example.globalexception.entity.Course;
import com.example.globalexception.repository.CourseRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CourseService {
private final CourseRepository courseRepository;
public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
public Course createCourse(Course course) {
if (course.getTitle() == null || course.getTitle().isBlank()) {
throw new InvalidDataException("Course title is required");
}
if (course.getDurationInMonths() == null || course.getDurationInMonths() < 1) {
throw new InvalidDataException("Course duration must be at least 1 month");
}
course.setId(null);
return courseRepository.save(course);
}
public List<Course> getAllCourses() {
return courseRepository.findAll();
}
public Course getCourseById(Long id) {
return courseRepository.findById(id)
.orElseThrow(() -> new CourseNotFoundException(id));
}
}
package com.example.globalexception.service;
public class DuplicateStudentException extends RuntimeException {
public DuplicateStudentException(String email) {
super("A student already exists with email " + email);
}
}
package com.example.globalexception.service;
public class CourseNotFoundException extends RuntimeException {
public CourseNotFoundException(Long id) {
super("Course not found with id " + id);
}
}
package com.example.globalexception.service;
// Thrown when request data breaks a rule (blank name, bad email, wrong age...)
public class InvalidDataException extends RuntimeException {
public InvalidDataException(String message) {
super(message);
}
}
package com.example.globalexception.service;
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(Long id) {
super("Student not found with id " + id);
}
}
Step 8: Create Controller class
package com.example.globalexception.controller;
import com.example.globalexception.entity.Student;
import com.example.globalexception.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
// No try-catch and no @ExceptionHandler here: GlobalExceptionHandler handles all errors
@RestController
@RequestMapping("/students")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@PostMapping
public ResponseEntity<Student> createStudent(@RequestBody Student student) {
return ResponseEntity.status(HttpStatus.CREATED).body(studentService.createStudent(student));
}
@GetMapping
public List<Student> getAllStudents() {
return studentService.getAllStudents();
}
@GetMapping("/{id}")
public Student getStudentById(@PathVariable Long id) {
return studentService.getStudentById(id);
}
@PutMapping("/{id}")
public Student updateStudent(@PathVariable Long id, @RequestBody Student student) {
return studentService.updateStudent(id, student);
}
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteStudent(@PathVariable Long id) {
studentService.deleteStudent(id);
return ResponseEntity.ok("Student deleted with id " + id);
}
}
package com.example.globalexception.controller;
import com.example.globalexception.entity.Course;
import com.example.globalexception.service.CourseService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
// A second controller: it is also covered by the same GlobalExceptionHandler
@RestController
@RequestMapping("/courses")
public class CourseController {
private final CourseService courseService;
public CourseController(CourseService courseService) {
this.courseService = courseService;
}
@PostMapping
public ResponseEntity<Course> createCourse(@RequestBody Course course) {
return ResponseEntity.status(HttpStatus.CREATED).body(courseService.createCourse(course));
}
@GetMapping
public List<Course> getAllCourses() {
return courseService.getAllCourses();
}
@GetMapping("/{id}")
public Course getCourseById(@PathVariable Long id) {
return courseService.getCourseById(id);
}
}
package com.example.globalexception.controller;
import com.example.globalexception.service.CourseNotFoundException;
import com.example.globalexception.service.DuplicateStudentException;
import com.example.globalexception.service.InvalidDataException;
import com.example.globalexception.service.StudentNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
// One central class that handles exceptions thrown from ALL controllers
@RestControllerAdvice
public class GlobalExceptionHandler {
// 404 - one handler for more than one exception type
@ExceptionHandler({StudentNotFoundException.class, CourseNotFoundException.class})
public ResponseEntity<Map<String, Object>> handleNotFound(RuntimeException ex, HttpServletRequest request) {
return buildError(HttpStatus.NOT_FOUND, ex.getMessage(), request);
}
// 400 - invalid data
@ExceptionHandler(InvalidDataException.class)
public ResponseEntity<Map<String, Object>> handleInvalidData(InvalidDataException ex, HttpServletRequest request) {
return buildError(HttpStatus.BAD_REQUEST, ex.getMessage(), request);
}
// 409 - duplicate email
@ExceptionHandler(DuplicateStudentException.class)
public ResponseEntity<Map<String, Object>> handleDuplicate(DuplicateStudentException ex, HttpServletRequest request) {
return buildError(HttpStatus.CONFLICT, ex.getMessage(), request);
}
// 400 - broken JSON body, e.g. missing quote or comma
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleMalformedJson(HttpMessageNotReadableException ex, HttpServletRequest request) {
return buildError(HttpStatus.BAD_REQUEST, "Malformed JSON request body", request);
}
// 400 - wrong type in URL, e.g. /students/abc
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Map<String, Object>> handleTypeMismatch(MethodArgumentTypeMismatchException ex, HttpServletRequest request) {
String message = "Invalid value '" + ex.getValue() + "' for parameter '" + ex.getName() + "'";
return buildError(HttpStatus.BAD_REQUEST, message, request);
}
// 500 - safety net for any other unexpected exception
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleAnyException(Exception ex, HttpServletRequest request) {
return buildError(HttpStatus.INTERNAL_SERVER_ERROR, "Something went wrong. Please try again later", request);
}
private ResponseEntity<Map<String, Object>> buildError(HttpStatus status, String message, HttpServletRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now().toString());
body.put("status", status.value());
body.put("error", status.getReasonPhrase());
body.put("message", message);
body.put("path", request.getRequestURI());
return ResponseEntity.status(status).body(body);
}
}
Step 9: Main Application
package com.example.globalexception;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentGlobalExceptionApplication {
public static void main(String[] args) {
SpringApplication.run(StudentGlobalExceptionApplication.class, args);
}
}
Step 10: Run the StudentGlobalExceptionApplication


