17 - Exception Handling
เป้าหมายของบท
Section titled “เป้าหมายของบท”บทก่อนหน้าเราเพิ่ม validation แล้ว แต่ error response จาก Spring Boot ยังอาจอ่านยาก บทนี้จะสร้างระบบจัดการ error กลางด้วย @RestControllerAdvice
หลังจบบทนี้ผู้อ่านควรทำได้:
- สร้าง custom exception
- แปลง exception เป็น HTTP status ที่เหมาะสม
- จัดการ validation error จาก
@Valid - ส่ง error response ที่มี
status,message,path,timestamp - เลิกโยน
RuntimeExceptionแบบกว้าง ๆ ใน service
ปัญหาของ RuntimeException
Section titled “ปัญหาของ RuntimeException”ในบทก่อน service อาจมีโค้ดแบบนี้:
throw new RuntimeException("User not found");ปัญหาคือ RuntimeException ไม่ได้บอกว่า error นี้ควรเป็น HTTP status อะไร
กรณี user ไม่มีอยู่ควรเป็น:
404 Not Foundกรณี username/email ซ้ำควรเป็น:
409 Conflictกรณี request body ผิด validation ควรเป็น:
400 Bad Requestดังนั้นเราจะสร้าง exception ให้ชัดขึ้น
สร้าง UserNotFoundException
Section titled “สร้าง UserNotFoundException”สร้างไฟล์:
src/main/java/com/example/backendapi/exception/UserNotFoundException.javapackage com.example.backendapi.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) { super("User not found with id: " + id); }}สร้าง DuplicateUserException
Section titled “สร้าง DuplicateUserException”สร้างไฟล์:
src/main/java/com/example/backendapi/exception/DuplicateUserException.javapackage com.example.backendapi.exception;
public class DuplicateUserException extends RuntimeException {
public DuplicateUserException(String message) { super(message); }}ชื่อ exception ควรสื่อปัญหาให้ชัด ไม่ใช่ทุกอย่างเป็น RuntimeException
สร้าง ErrorResponse
Section titled “สร้าง ErrorResponse”สร้างไฟล์:
src/main/java/com/example/backendapi/common/ErrorResponse.javapackage com.example.backendapi.common;
import java.time.LocalDateTime;import java.util.Map;
public record ErrorResponse( boolean success, int status, String message, String path, LocalDateTime timestamp, Map<String, String> errors) { public static ErrorResponse of(int status, String message, String path) { return new ErrorResponse(false, status, message, path, LocalDateTime.now(), null); }
public static ErrorResponse validation( int status, String message, String path, Map<String, String> errors ) { return new ErrorResponse(false, status, message, path, LocalDateTime.now(), errors); }}field errors ใช้สำหรับ validation error หลาย field เช่น username, email, password
สร้าง GlobalExceptionHandler
Section titled “สร้าง GlobalExceptionHandler”สร้างไฟล์:
src/main/java/com/example/backendapi/exception/GlobalExceptionHandler.javapackage com.example.backendapi.exception;
import com.example.backendapi.common.ErrorResponse;import jakarta.servlet.http.HttpServletRequest;import java.util.LinkedHashMap;import java.util.Map;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvicepublic class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ErrorResponse> handleUserNotFound( UserNotFoundException ex, HttpServletRequest request ) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(ErrorResponse.of(404, ex.getMessage(), request.getRequestURI())); }
@ExceptionHandler(DuplicateUserException.class) public ResponseEntity<ErrorResponse> handleDuplicateUser( DuplicateUserException ex, HttpServletRequest request ) { return ResponseEntity.status(HttpStatus.CONFLICT) .body(ErrorResponse.of(409, ex.getMessage(), request.getRequestURI())); }
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidation( MethodArgumentNotValidException ex, HttpServletRequest request ) { Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()) );
return ResponseEntity.badRequest() .body(ErrorResponse.validation( 400, "Validation failed", request.getRequestURI(), errors )); }}ปรับ Service ให้ใช้ custom exception
Section titled “ปรับ Service ให้ใช้ custom exception”ตัวอย่าง:
public User findById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException(id));}กรณีข้อมูลซ้ำ:
if (userRepository.existsByEmail(request.email())) { throw new DuplicateUserException("Email already exists");}ตัวอย่าง response เมื่อ user ไม่มีอยู่
Section titled “ตัวอย่าง response เมื่อ user ไม่มีอยู่”{ "success": false, "status": 404, "message": "User not found with id: 99", "path": "/api/v1/users/99", "timestamp": "2026-06-05T15:30:00", "errors": null}ตัวอย่าง response เมื่อ validation ไม่ผ่าน
Section titled “ตัวอย่าง response เมื่อ validation ไม่ผ่าน”{ "success": false, "status": 400, "message": "Validation failed", "path": "/api/v1/users", "timestamp": "2026-06-05T15:30:00", "errors": { "username": "Username is required", "email": "Email format is invalid", "password": "Password must be at least 8 characters" }}ควรจับ Exception.class ไหม
Section titled “ควรจับ Exception.class ไหม”ในช่วงเริ่มเรียนยังไม่จำเป็นต้องจับทุก exception
ถ้าจับ Exception.class เราอาจซ่อน bug จริงของระบบจน debug ยาก ในงานจริงอาจมี handler กลางสำหรับ unexpected error แต่ต้อง log รายละเอียดไว้ และไม่ส่ง stack trace ให้ client
แบบฝึกหัดท้ายบท
Section titled “แบบฝึกหัดท้ายบท”- สร้าง
UserNotFoundException - สร้าง
DuplicateUserException - สร้าง
ErrorResponse - สร้าง
GlobalExceptionHandler - ทดสอบ
GET /api/v1/users/999 - ทดสอบสร้าง user ด้วย email ซ้ำ
- ทดสอบ validation error