Exception Handling trong Java: Xử Lý Lỗi Chuyên Nghiệp

Exception là gì?

Exception (ngoại lệ) là sự kiện bất thường xảy ra trong quá trình thực thi chương trình, làm gián đoạn luồng code bình thường. Java cung cấp cơ chế mạnh mẽ để xử lý exceptions một cách có cấu trúc.

Phân loại Exception trong Java

1. Checked Exception

Exceptions được kiểm tra tại compile-time, bắt buộc phải xử lý:

// Phải xử lý IOException
public void readFile(String path) throws IOException {
    FileReader file = new FileReader(path);
    BufferedReader reader = new BufferedReader(file);
    // ...
}

Ví dụ: IOException, SQLException, ClassNotFoundException

2. Unchecked Exception (Runtime Exception)

Exceptions xảy ra tại runtime, không bắt buộc xử lý:

int[] arr = new int[5];
arr[10] = 50; // ArrayIndexOutOfBoundsException

Ví dụ: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException

3. Error

Lỗi nghiêm trọng, thường không nên xử lý:

  • OutOfMemoryError
  • StackOverflowError

Cú pháp Try-Catch-Finally

Cơ bản

try {
    // Code có thể gây exception
    int result = 10 / 0;
} catch (ArithmeticException e) {
    // Xử lý lỗi
    System.out.println("Không thể chia cho 0: " + e.getMessage());
} finally {
    // Luôn được thực thi
    System.out.println("Cleanup code");
}

Multiple Catch Blocks

try {
    String str = null;
    System.out.println(str.length());
    int result = 10 / 0;
} catch (NullPointerException e) {
    System.out.println("Null pointer: " + e.getMessage());
} catch (ArithmeticException e) {
    System.out.println("Arithmetic error: " + e.getMessage());
} catch (Exception e) {
    // Catch tổng quát - đặt cuối cùng
    System.out.println("General error: " + e.getMessage());
}

Multi-catch (Java 7+)

try {
    // Code
} catch (IOException | SQLException e) {
    System.out.println("IO hoặc SQL error: " + e.getMessage());
}

Try-with-Resources (Java 7+)

Tự động đóng resources, không cần finally:

// Cách cũ
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("file.txt"));
    String line = reader.readLine();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// Cách mới (Java 7+)
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    String line = reader.readLine();
} catch (IOException e) {
    e.printStackTrace();
}

Lợi ích:

  • Code ngắn gọn hơn
  • Tự động close() resources
  • Không bị resource leak

Multiple Resources

try (FileInputStream fis = new FileInputStream("input.txt");
     FileOutputStream fos = new FileOutputStream("output.txt")) {
    // Sử dụng cả 2 streams
} catch (IOException e) {
    e.printStackTrace();
}

Throws và Throw

throws - Khai báo exception

public void readFile(String path) throws IOException, FileNotFoundException {
    FileReader file = new FileReader(path);
    // ...
}

throw - Ném exception

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Tuổi không thể âm!");
    }
    this.age = age;
}

Custom Exception

Tạo Exception riêng

public class InsufficientBalanceException extends Exception {
    private double amount;
    
    public InsufficientBalanceException(double amount) {
        super("Số dư không đủ: thiếu " + amount);
        this.amount = amount;
    }
    
    public double getAmount() {
        return amount;
    }
}

Sử dụng Custom Exception

public class BankAccount {
    private double balance;
    
    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException(amount - balance);
        }
        balance -= amount;
    }
}

// Sử dụng
try {
    account.withdraw(1000);
} catch (InsufficientBalanceException e) {
    System.out.println(e.getMessage());
    System.out.println("Thiếu: " + e.getAmount());
}

Exception Propagation

Exception được lan truyền lên call stack:

public class ExceptionPropagation {
    public void method1() {
        method2();
    }
    
    public void method2() {
        method3();
    }
    
    public void method3() {
        throw new RuntimeException("Lỗi ở method3");
    }
    
    public static void main(String[] args) {
        try {
            new ExceptionPropagation().method1();
        } catch (Exception e) {
            e.printStackTrace();
            // Stack trace sẽ hiển thị: method3 -> method2 -> method1 -> main
        }
    }
}

Best Practices

1. Đừng nuốt exception

// ❌ SỬA: Không làm thế này
try {
    riskyOperation();
} catch (Exception e) {
    // Không làm gì - exception bị "nuốt"
}

// ✅ TỐT: Ít nhất log lại
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("Error during risky operation", e);
}

2. Catch exception cụ thể

// ❌ SỬA
try {
    // code
} catch (Exception e) {
    // Quá chung chung
}

// ✅ TỐT
try {
    // code
} catch (IOException e) {
    // Xử lý IO error cụ thể
} catch (SQLException e) {
    // Xử lý SQL error cụ thể
}

3. Đóng resources đúng cách

// ❌ SỬA: Dùng finally thủ công
FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt");
} finally {
    if (fis != null) fis.close();
}

// ✅ TỐT: Dùng try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // code
}

4. Exception message rõ ràng

// ❌ SỬA
throw new IllegalArgumentException("Invalid");

// ✅ TỐT
throw new IllegalArgumentException(
    "Invalid age value: " + age + ". Age must be between 0 and 150"
);

5. Không dùng exception để control flow

// ❌ SỬA: Dùng exception như control flow
try {
    while (true) {
        array[i++] = getValue();
    }
} catch (ArrayIndexOutOfBoundsException e) {
    // Xong rồi
}

// ✅ TỐT: Dùng điều kiện bình thường
for (int i = 0; i < array.length; i++) {
    array[i] = getValue();
}

6. Log với context đầy đủ

try {
    processPayment(userId, amount);
} catch (PaymentException e) {
    logger.error(
        "Payment failed for user: {} with amount: {}", 
        userId, 
        amount, 
        e
    );
    throw e;
}

Ví dụ thực tế

1. Service Layer với Exception Handling

@Service
public class UserService {
    private UserRepository userRepository;
    
    public User getUserById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
    }
    
    public User createUser(UserDTO dto) {
        try {
            validateUser(dto);
            User user = new User(dto.getName(), dto.getEmail());
            return userRepository.save(user);
        } catch (ValidationException e) {
            logger.error("Validation failed for user: {}", dto, e);
            throw new BadRequestException(e.getMessage());
        } catch (DataAccessException e) {
            logger.error("Database error creating user: {}", dto, e);
            throw new ServiceException("Unable to create user", e);
        }
    }
}

2. REST Controller với Exception Handling

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        try {
            User user = userService.getUserById(id);
            return ResponseEntity.ok(user);
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            e.getMessage(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

3. Retry với Exception

public <T> T executeWithRetry(Supplier<T> operation, int maxRetries) {
    int attempts = 0;
    Exception lastException = null;
    
    while (attempts < maxRetries) {
        try {
            return operation.get();
        } catch (Exception e) {
            lastException = e;
            attempts++;
            logger.warn("Attempt {} failed, retrying...", attempts);
            
            if (attempts < maxRetries) {
                try {
                    Thread.sleep(1000 * attempts); // Exponential backoff
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(ie);
                }
            }
        }
    }
    
    throw new RuntimeException("Failed after " + maxRetries + " attempts", lastException);
}

Kết luận

Exception handling là kỹ năng quan trọng trong Java:

  • Hiểu rõ checked vs unchecked exceptions
  • Sử dụng try-with-resources cho resource management
  • Tạo custom exceptions khi cần
  • Follow best practices để code dễ maintain
  • Log exceptions đầy đủ context

Xử lý exception đúng cách giúp ứng dụng robust và dễ debug hơn!