Skillversitas

Exception Handling

Introduction

Good exception handling ensures your API always returns consistent and informative error responses. Spring Boot provides a centralized exception-handling mechanism.

@ExceptionHandler

Used inside a controller to handle specific exceptions.

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

    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) {
        return userService.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User not found"));
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

@RestControllerAdvice

Handles exceptions globally across all controllers. Place it in a separate class.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(404, ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .collect(Collectors.joining(", "));
        ErrorResponse error = new ErrorResponse(400, message);
        return ResponseEntity.badRequest().body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        ErrorResponse error = new ErrorResponse(500, "An internal server error occurred");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Custom Exception Classes

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
        super(message);
    }
}

Consistent Error Responses

public class ErrorResponse {
    private int status;
    private String message;
    private LocalDateTime timestamp;

    public ErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
        this.timestamp = LocalDateTime.now();
    }

    // getters and setters
}

Example response:

{
  "status": 404,
  "message": "User with id 99 not found",
  "timestamp": "2026-07-16T10:30:00"
}

@ResponseStatus

Sets the HTTP status code directly on the exception class, without needing ResponseEntity.

@ResponseStatus(HttpStatus.CONFLICT) // 409
public class DuplicateEntryException extends RuntimeException {
    public DuplicateEntryException(String message) {
        super(message);
    }
}

Exception Handling Order

Priority Mechanism Use Case
1 @ExceptionHandler in the controller Controller-specific errors
2 @RestControllerAdvice Global errors
3 Spring default Unhandled errors

Summary

  • Use @RestControllerAdvice for global handling.
  • Create custom exceptions for domain-specific errors.
  • Always return a consistent error response.

Next chapter: We'll cover Spring Data JPA — how Spring Boot simplifies database access using JPA and Hibernate.