Controller-level Handling

Controller-level handling means a controller catches and handles exceptions itself, using @ExceptionHandler methods written inside the controller class. When a service throws an exception, Spring looks in that same controller for a matching handler and uses it to build the response. It is used to turn failures such as a missing student or invalid data into clear HTTP status codes and JSON messages, instead of a generic 500 error page. The handlers apply only to the controller that contains them, so each controller can treat errors in its own way.

Features

  • @ExceptionHandler methods placed inside the controller
  • One handler per exception type, each with its own status code
  • Custom JSON error body (timestamp, status, error, message)
  • Controller methods stay clean, with no try-catch blocks
  • Works with custom exceptions thrown from the service layer
  • Limited to one controller, so other controllers are not affected

Controller-level Handling Types

  • Single exception handler: @ExceptionHandler(StudentNotFoundException.class) handles one exception type. This example uses one for each of its three exceptions.
  • Multiple exceptions in one handler: @ExceptionHandler({A.class, B.class}) lets one method handle several exceptions that need the same response.
  • Handler returning ResponseEntity: the method builds the status and a custom body, which is the style used here.
  • Handler with @ResponseStatus: the method is annotated with a fixed status, and its return value is the body.
  • Manual try-catch in the endpoint: the controller method catches the exception itself. It works but repeats code in every endpoint, so @ExceptionHandler is preferred.

Hands-on Experiment: Creating a spring boot application to understand how individual controllers can handle specific application exceptions.

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: controllerhandling
  • Name: controllerhandling
  • Package name: com.example.controllerhandling
  • 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-controller-handling-demo</artifactId>
    <version>1.0.0</version>
    <name>student-controller-handling-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-controller-handling-demo
server.port=8082

# MySQL (database is created automatically if missing)
spring.datasource.url=jdbc:mysql://localhost:3306/student_handling_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=trueCode language: PHP (php)

Step 5: Create Entity Class

package com.example.controllerhandling.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 String course;
    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 String getCourse() { return course; }
    public void setCourse(String course) { this.course = course; }

    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
}

Step 6: Create below Repository Interface

package com.example.controllerhandling.repository;

import com.example.controllerhandling.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);
}

Step 7: Create below Service class

package com.example.controllerhandling.service;

import com.example.controllerhandling.entity.Student;
import com.example.controllerhandling.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.setCourse(updated.getCourse());
        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 InvalidStudentException("Student name is required");
        }
        if (student.getEmail() == null || !student.getEmail().contains("@")) {
            throw new InvalidStudentException("A valid email is required");
        }
        if (student.getAge() == null || student.getAge() < 15 || student.getAge() > 60) {
            throw new InvalidStudentException("Age must be between 15 and 60");
        }
    }
}
package com.example.controllerhandling.service;

// Thrown when another student already uses the same email
public class DuplicateStudentException extends RuntimeException {

    public DuplicateStudentException(String email) {
        super("A student already exists with email " + email);
    }
}
package com.example.controllerhandling.service;

// Thrown when student data breaks a rule (blank name, bad email, wrong age)
public class InvalidStudentException extends RuntimeException {

    public InvalidStudentException(String message) {
        super(message);
    }
}
package com.example.controllerhandling.service;

// Thrown when a student id does not exist
public class StudentNotFoundException extends RuntimeException {

    public StudentNotFoundException(Long id) {
        super("Student not found with id " + id);
    }
}

Step 8: Create Controller class

package com.example.controllerhandling.controller;

import com.example.controllerhandling.entity.Student;
import com.example.controllerhandling.service.DuplicateStudentException;
import com.example.controllerhandling.service.InvalidStudentException;
import com.example.controllerhandling.service.StudentNotFoundException;
import com.example.controllerhandling.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.ExceptionHandler;
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.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    // ---------- Endpoints ----------

    @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);
    }

    // ---------- Controller-level exception handlers ----------
    // These methods work only for exceptions thrown from THIS controller.

    @ExceptionHandler(StudentNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleNotFound(StudentNotFoundException ex) {
        return buildError(HttpStatus.NOT_FOUND, ex.getMessage());
    }

    @ExceptionHandler(InvalidStudentException.class)
    public ResponseEntity<Map<String, Object>> handleInvalid(InvalidStudentException ex) {
        return buildError(HttpStatus.BAD_REQUEST, ex.getMessage());
    }

    @ExceptionHandler(DuplicateStudentException.class)
    public ResponseEntity<Map<String, Object>> handleDuplicate(DuplicateStudentException ex) {
        return buildError(HttpStatus.CONFLICT, ex.getMessage());
    }

    private ResponseEntity<Map<String, Object>> buildError(HttpStatus status, String message) {
        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);
        return ResponseEntity.status(status).body(body);
    }
}

Step 9: Main Application

package com.example.controllerhandling;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StudentControllerHandlingApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudentControllerHandlingApplication.class, args);
    }
}

Step 10: Run the StudentControllerHandlerApplication

Scroll to Top