Chuyển tới nội dung chính

Custom Exceptions

Bài trước: try-catch-finally — Đã học cách bắt và xử lý exception với try-catch-finally. Bài này sẽ học cách tạo custom exception và sử dụng throw/throws cho business logic riêng.

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Phân biệt throw (ném exception) vs throws (khai báo exception)
  • Tạo custom exception class extends Exception (checked) hoặc RuntimeException (unchecked)
  • Áp dụng exception chaining để preserve thông tin exception gốc
  • Thêm custom fields và methods vào exception class cho business logic
  • Thiết kế exception hierarchy cho domain-specific errors

throw keyword

throw dùng để ném (throw) một exception một cách tường minh.

Cú pháp

throw new ExceptionType("Error message");

Ví dụ cơ bản

public class ThrowExample {
public static void validateAge(int age) {
if (age < 18) {
// ✅ Throw exception khi điều kiện không hợp lệ
throw new IllegalArgumentException("Age must be at least 18");
}
System.out.println("Age is valid: " + age);
}

public static void main(String[] args) {
try {
validateAge(15); // ❌ Throw exception
System.out.println("This line won't execute");
} catch (IllegalArgumentException e) {
System.out.println("Validation error: " + e.getMessage());
}
}
}

Output:

Validation error: Age must be at least 18

Khi nào dùng throw?

  1. Validation: Kiểm tra input không hợp lệ
  2. Business logic violation: Vi phạm quy tắc nghiệp vụ
  3. Re-throwing exception: Throw lại exception sau khi log/xử lý
  4. Early return: Thoát method sớm khi có lỗi
public class BankAccount {
private double balance;

public void withdraw(double amount) {
// Validation với throw
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}

if (amount > balance) {
throw new IllegalStateException("Insufficient balance");
}

balance -= amount;
System.out.println("Withdrawn: " + amount);
}
}

throws keyword

throws khai báo trong method signature rằng method này có thể throw exception. Caller phải xử lý exception đó.

Cú pháp

public void methodName() throws ExceptionType1, ExceptionType2 {
// Code có thể throw exception
}

Ví dụ

import java.io.*;

public class ThrowsExample {
// ✅ Khai báo throws IOException
public static void readFile(String filename) throws IOException {
FileReader reader = new FileReader(filename); // Có thể throw IOException
// read file
reader.close();
}

public static void main(String[] args) {
try {
// ⚠️ Caller phải handle IOException
readFile("data.txt");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}

Khi nào dùng throws?

  1. Checked exception: Bắt buộc phải khai báo throws hoặc try-catch
  2. Delegate responsibility: Để caller xử lý exception
  3. Multiple methods: Nhiều methods cùng throw exception, xử lý tập trung ở caller
public class DataProcessor {
// ✅ Propagate exception lên caller
public void processFile(String filename) throws IOException {
readFile(filename); // throws IOException
parseFile(filename); // throws IOException
}

private void readFile(String filename) throws IOException {
// implementation
}

private void parseFile(String filename) throws IOException {
// implementation
}
}

throw vs throws

Đặc điểmthrowthrows
Vị tríBên trong method bodyMethod signature
Mục đíchNém exceptionKhai báo exception có thể bị ném
Syntaxthrow new Exception()throws Exception
Số lượngMột exception mỗi lầnNhiều exception types (phân cách ,)
Sử dụngTạo và ném exception objectKhai báo method có thể throw exception
Theo sauException object (new ...)Exception class name

Ví dụ minh họa

public class ThrowVsThrows {
// throws: Khai báo trong signature
public void method1() throws IOException, SQLException {
// throw: Ném exception object
throw new IOException("File not found");
}

// throws: Có thể khai báo nhiều exception
public void method2() throws Exception {
// throw: Chỉ ném 1 exception mỗi lần
if (someCondition) {
throw new IOException("I/O error");
} else {
throw new SQLException("Database error");
}
}

// Không khai báo throws → Phải handle trong method
public void method3() {
try {
throw new IOException("Must be handled");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Nhớ dễ hơn
  • throw = "ném bóng" (ném exception object)
  • throws = "có thể ném bóng" (khai báo method có thể ném)

Tạo Custom Exception Class

Khi built-in exception không đủ diễn đạt business logic, hãy tạo custom exception.

extends Exception (Checked Exception)

/**
* Custom CHECKED exception
* Compiler bắt buộc caller phải xử lý
*/
public class InsufficientBalanceException extends Exception {
// Constructor với message
public InsufficientBalanceException(String message) {
super(message);
}

// Constructor với message và cause
public InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}

Sử dụng:

public class BankAccount {
private double balance;

public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
// ✅ Throw custom checked exception
throw new InsufficientBalanceException(
"Cannot withdraw " + amount + ", balance: " + balance
);
}
balance -= amount;
}
}

// Caller PHẢI xử lý
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
try {
account.withdraw(1000); // ⚠️ Phải try-catch
} catch (InsufficientBalanceException e) {
System.out.println("Transaction failed: " + e.getMessage());
}
}
}

extends RuntimeException (Unchecked Exception)

/**
* Custom UNCHECKED exception
* Không bắt buộc phải xử lý
*/
public class InvalidEmailException extends RuntimeException {
public InvalidEmailException(String message) {
super(message);
}

public InvalidEmailException(String message, Throwable cause) {
super(message, cause);
}
}

Sử dụng:

public class User {
private String email;

public void setEmail(String email) {
// Validation logic
if (!email.contains("@")) {
// ✅ Throw custom unchecked exception
throw new InvalidEmailException("Email must contain @: " + email);
}
this.email = email;
}
}

// Caller KHÔNG BẮT BUỘC xử lý
public class Main {
public static void main(String[] args) {
User user = new User();
user.setEmail("invalid-email"); // ❌ Runtime exception!
// Hoặc có thể xử lý nếu muốn
}
}
Checked vs Unchecked: Chọn loại nào?

Checked Exception (extends Exception):

  • Lỗi có thể recover được
  • Lỗi bên ngoài tầm kiểm soát (I/O, network, database)
  • Ví dụ: InsufficientBalanceException, UserNotFoundException

Unchecked Exception (extends RuntimeException):

  • Lỗi lập trình (logic bug)
  • Lỗi validation có thể prevent bằng code tốt hơn
  • Ví dụ: InvalidEmailException, InvalidAgeException

Best Practices khi tạo Custom Exception

1. Đặt tên rõ ràng, suffix "Exception"

// ✅ GOOD: Rõ ràng, có suffix "Exception"
public class UserNotFoundException extends Exception { }
public class InvalidPasswordException extends RuntimeException { }
public class PaymentFailedException extends Exception { }

// ❌ BAD: Không rõ ràng, thiếu "Exception"
public class UserNotFound extends Exception { }
public class InvalidPassword extends RuntimeException { }
public class PaymentFailed extends Exception { }

2. Cung cấp nhiều constructors

public class OrderProcessingException extends Exception {
// Constructor 1: Chỉ có message
public OrderProcessingException(String message) {
super(message);
}

// Constructor 2: Message + cause (exception chaining)
public OrderProcessingException(String message, Throwable cause) {
super(message, cause);
}

// Constructor 3: Chỉ có cause
public OrderProcessingException(Throwable cause) {
super(cause);
}

// Constructor 4: No-arg (ít dùng)
public OrderProcessingException() {
super("Order processing failed");
}
}

3. Thêm custom fields nếu cần

public class ValidationException extends RuntimeException {
private final String fieldName; // ✅ Field bị lỗi
private final Object invalidValue; // ✅ Giá trị không hợp lệ

public ValidationException(String fieldName, Object invalidValue, String message) {
super(message);
this.fieldName = fieldName;
this.invalidValue = invalidValue;
}

public String getFieldName() {
return fieldName;
}

public Object getInvalidValue() {
return invalidValue;
}

@Override
public String toString() {
return String.format("ValidationException: field='%s', value='%s', message='%s'",
fieldName, invalidValue, getMessage());
}
}

// Sử dụng
throw new ValidationException("email", "invalid@", "Email must contain domain");

4. Viết Javadoc rõ ràng

/**
* Thrown when a user is not found in the system.
*
* <p>This exception indicates that the requested user does not exist
* in the database or has been deleted.</p>
*
* <p>Example usage:</p>
* <pre>{@code
* User user = userRepository.findById(userId)
* .orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
* }</pre>
*
* @since 1.0
* @see UserRepository
*/
public class UserNotFoundException extends Exception {
/**
* Constructs a new UserNotFoundException with the specified detail message.
*
* @param message the detail message
*/
public UserNotFoundException(String message) {
super(message);
}
}

Exception Chaining (Wrapping)

Exception chaining là kỹ thuật wrap exception gốc trong một exception khác để preserve context.

Tại sao cần Exception Chaining?

// ❌ BAD: Mất thông tin exception gốc
public void processData() throws DataProcessingException {
try {
// Low-level operation
readFromDatabase();
} catch (SQLException e) {
// ❌ Mất thông tin SQLException
throw new DataProcessingException("Failed to process data");
}
}

// ✅ GOOD: Preserve exception gốc
public void processData() throws DataProcessingException {
try {
readFromDatabase();
} catch (SQLException e) {
// ✅ Wrap SQLException, giữ nguyên cause
throw new DataProcessingException("Failed to process data", e);
}
}

Ví dụ thực tế

public class UserService {
private DatabaseConnection db;

public User getUserById(String userId) throws UserServiceException {
try {
// Layer 1: Database access
String query = "SELECT * FROM users WHERE id = ?";
ResultSet rs = db.executeQuery(query, userId);

if (!rs.next()) {
// Business logic exception
throw new UserNotFoundException("User not found: " + userId);
}

return mapResultSetToUser(rs);

} catch (SQLException e) {
// ✅ Wrap low-level SQLException
throw new UserServiceException(
"Database error while fetching user: " + userId,
e // ← Preserve original cause
);

} catch (UserNotFoundException e) {
// ✅ Re-throw business exception với context
throw new UserServiceException(
"Failed to get user: " + userId,
e // ← Chain exception
);
}
}
}

// Sử dụng
try {
User user = userService.getUserById("123");
} catch (UserServiceException e) {
System.out.println("Error: " + e.getMessage());

// ✅ Truy cập exception gốc
Throwable cause = e.getCause();
if (cause instanceof SQLException) {
System.out.println("Database error: " + cause.getMessage());
}

// ✅ Print full stack trace (bao gồm cả cause)
e.printStackTrace();
}

Accessing Cause

try {
// some operation
} catch (Exception e) {
// Lấy exception gốc
Throwable cause = e.getCause();

// Lấy root cause (đi sâu nhất)
Throwable rootCause = getRootCause(e);
}

// Utility method
public static Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable;
while (cause.getCause() != null) {
cause = cause.getCause();
}
return cause;
}

Ví dụ thực tế: E-commerce System

// ============= Custom Exceptions =============

/**
* Base exception cho Product-related errors
*/
public class ProductException extends Exception {
public ProductException(String message) {
super(message);
}

public ProductException(String message, Throwable cause) {
super(message, cause);
}
}

/**
* Product không tồn tại
*/
public class ProductNotFoundException extends ProductException {
private final String productId;

public ProductNotFoundException(String productId) {
super("Product not found: " + productId);
this.productId = productId;
}

public String getProductId() {
return productId;
}
}

/**
* Out of stock
*/
public class OutOfStockException extends ProductException {
private final String productId;
private final int requestedQuantity;
private final int availableQuantity;

public OutOfStockException(String productId, int requestedQuantity, int availableQuantity) {
super(String.format(
"Out of stock: product=%s, requested=%d, available=%d",
productId, requestedQuantity, availableQuantity
));
this.productId = productId;
this.requestedQuantity = requestedQuantity;
this.availableQuantity = availableQuantity;
}

public int getRequestedQuantity() { return requestedQuantity; }
public int getAvailableQuantity() { return availableQuantity; }
}

/**
* Invalid price
*/
public class InvalidPriceException extends RuntimeException {
private final double price;

public InvalidPriceException(double price) {
super("Price must be positive: " + price);
this.price = price;
}

public double getPrice() {
return price;
}
}

// ============= Service Layer =============

public class ProductService {
private Map<String, Product> productDatabase = new HashMap<>();

public Product getProduct(String productId) throws ProductNotFoundException {
Product product = productDatabase.get(productId);
if (product == null) {
throw new ProductNotFoundException(productId);
}
return product;
}

public void addToCart(String productId, int quantity)
throws ProductNotFoundException, OutOfStockException {

// Validate product exists
Product product = getProduct(productId); // Can throw ProductNotFoundException

// Validate stock
if (product.getStock() < quantity) {
throw new OutOfStockException(
productId,
quantity,
product.getStock()
);
}

// Add to cart logic...
System.out.println("Added to cart: " + product.getName());
}

public void updatePrice(String productId, double newPrice)
throws ProductNotFoundException {

// Validate price (unchecked exception)
if (newPrice <= 0) {
throw new InvalidPriceException(newPrice);
}

Product product = getProduct(productId);
product.setPrice(newPrice);
}
}

// ============= Controller/Main =============

public class ShoppingApp {
public static void main(String[] args) {
ProductService service = new ProductService();

try {
// Test case 1: Product not found
service.addToCart("INVALID_ID", 1);

} catch (ProductNotFoundException e) {
System.err.println("Product error: " + e.getMessage());
System.err.println("Product ID: " + e.getProductId());

} catch (OutOfStockException e) {
System.err.println("Stock error: " + e.getMessage());
System.err.println("Available: " + e.getAvailableQuantity());
System.err.println("Requested: " + e.getRequestedQuantity());

} catch (ProductException e) {
// ✅ Catch parent exception cho các lỗi khác
System.err.println("Unexpected product error: " + e.getMessage());
}

try {
// Test case 2: Invalid price (unchecked)
service.updatePrice("PROD_001", -100); // ❌ InvalidPriceException

} catch (InvalidPriceException e) {
// Optional: Xử lý unchecked exception
System.err.println("Validation error: " + e.getMessage());

} catch (ProductNotFoundException e) {
System.err.println("Product not found: " + e.getMessage());
}
}
}

Exception Translation Pattern

Trong ứng dụng nhiều layer, bạn không muốn exception từ layer thấp "lộ ra" ở layer cao. Ví dụ: Controller không nên biết về SQLException — đó là chi tiết implementation của DAO layer.

Exception Translation là pattern chuyển đổi exception giữa các layer:

Mỗi layer chuyển đổi exception sang abstraction phù hợp, giấu implementation details của layer dưới.

Ví dụ thực tế

// === DAO Layer ===
public class UserDao {
public User findById(String id) throws SQLException {
// JDBC code, throw SQLException nếu lỗi DB
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
if (!rs.next()) return null;
return mapRow(rs);
}
}

// === Service Layer: Chuyển SQLException → DataAccessException ===
public class UserService {
private UserDao userDao;

public User getUser(String id) {
try {
User user = userDao.findById(id);
if (user == null) {
throw new UserNotFoundException("User not found: " + id);
}
return user;
} catch (SQLException e) {
// ✅ Translate: SQLException → DataAccessException
throw new DataAccessException("Failed to fetch user: " + id, e);
}
}
}

// === Controller Layer: Chuyển thành API response ===
public class UserController {
private UserService userService;

public ApiResponse getUser(String id) {
try {
User user = userService.getUser(id);
return ApiResponse.ok(user);
} catch (UserNotFoundException e) {
return ApiResponse.notFound(e.getMessage());
} catch (DataAccessException e) {
return ApiResponse.serverError("Internal server error");
}
}
}
Lợi ích
  • Giấu implementation details: Controller không biết dùng JDBC hay Hibernate
  • Dễ thay đổi: Đổi database → chỉ sửa DAO layer, Service/Controller không đổi
  • Log đúng chỗ: Mỗi layer log ở mức phù hợp

Serializable và serialVersionUID

Custom exceptions nên khai báo serialVersionUIDException implements Serializable:

public class OrderException extends Exception {
private static final long serialVersionUID = 1L;

private final String orderId;

public OrderException(String message, String orderId) {
super(message);
this.orderId = orderId;
}
}

Nếu thiếu serialVersionUID, JVM sẽ tự generate — nhưng nếu class thay đổi, deserialization có thể fail với InvalidClassException.

OCP Trap — Exception trong Constructor

Nếu constructor throw exception, object không được tạo. Tuy nhiên, constructor của parent class đã chạy (vì super() gọi trước).

class Resource {
Resource() {
System.out.println("Resource created");
}
}

class MyClass {
Resource resource;

MyClass() {
resource = new Resource(); // Resource được tạo
throw new RuntimeException("Oops!"); // MyClass KHÔNG được tạo
// resource không bao giờ được cleanup!
}
}

Hậu quả: Resource đã allocate trong constructor nhưng object chưa hoàn thành → resource bị leak. Đây là lý do nên dùng try-with-resources hoặc factory method thay vì allocate resources trong constructor.

Tóm tắt

Khái niệmMô tảVí dụ
throwNém exception objectthrow new Exception("error")
throwsKhai báo method có thể throwvoid method() throws IOException
extends ExceptionTạo checked exceptionBắt buộc xử lý
extends RuntimeExceptionTạo unchecked exceptionKhông bắt buộc xử lý
Exception chainingWrap exception, preserve causenew Exception(msg, cause)
Custom fieldsThêm metadata vào exceptionproductId, quantity, etc.
Exception TranslationChuyển exception giữa layersSQLExceptionDataAccessException

Bài tập

  1. Bài 1: Tạo custom exception InvalidAgeException (unchecked) với field age. Viết method validateAge(int age) throw exception nếu age < 0 hoặc age > 150.

  2. Bài 2: Tạo custom exception InsufficientFundsException (checked) với fields: accountId, balance, requestedAmount. Viết method withdraw() sử dụng exception này.

  3. Bài 3: Giải thích sự khác biệt:

throw new IOException("error");     // vs
throws IOException // vs
  1. Bài 4: Tạo exception hierarchy cho Library System:

    • LibraryException (base)
    • BookNotFoundException (checked)
    • BookAlreadyBorrowedException (checked)
    • InvalidISBNException (unchecked)
  2. Bài 5: Viết mini project File Validator với custom exceptions:

    • FileValidationException (base checked exception)
    • FileTooLargeException (checked, thêm field fileSize, maxSize)
    • UnsupportedFileTypeException (checked, thêm field fileType, supportedTypes)
    • Viết method validateFile(File file) throw các exception phù hợp
Bài tiếp theo

Trong bài tiếp theo, chúng ta sẽ học Best Practices để xử lý exception hiệu quả và professional!

Đọc thêm