Best Practices xử lý Exception
Bài trước: Custom Exceptions — Đã học cách tạo custom exception và sử dụng throw/throws. Bài này sẽ học các best practices và anti-patterns khi xử lý exception để viết code professional.
Sau bài này, bạn sẽ:
- Biết khi nào dùng Checked Exception vs Unchecked Exception
- Tránh các anti-pattern: catch quá generic, swallow exception, exception for flow control
- Log exception đúng cách với đủ context và không duplicate logging
- Áp dụng defensive programming kết hợp exception handling
- Viết clean code với guard clauses, early return, và fail fast principle
Khi nào dùng Checked vs Unchecked Exception?
Checked Exception (extends Exception)
Sử dụng khi:
- Lỗi có thể recover được
- Lỗi bên ngoài tầm kiểm soát của developer (I/O, network, database)
- Muốn bắt buộc caller xử lý exception
// ✅ GOOD: Checked exception cho I/O operations
public void saveToFile(String data) throws IOException {
FileWriter writer = new FileWriter("data.txt");
writer.write(data);
writer.close();
}
// Caller bắt buộc phải xử lý
try {
saveToFile("important data");
} catch (IOException e) {
// Có thể retry, log, hoặc fallback
System.err.println("Failed to save, retrying...");
}
Unchecked Exception (extends RuntimeException)
Sử dụng khi:
- Lỗi lập trình (programming bug)
- Lỗi có thể prevent bằng code tốt hơn
- Validation errors mà developer nên kiểm tra trước
// ✅ GOOD: Unchecked exception cho validation
public void setAge(int age) {
if (age < 0) {
// Runtime exception - developer NENBÊN check trước khi call
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
this.age = age;
}
// Caller không bắt buộc xử lý (nhưng nên validate trước)
user.setAge(25); // OK
Bảng so sánh chi tiết
| Tình huống | Exception Type | Ví dụ |
|---|---|---|
| File không tồn tại | Checked | FileNotFoundException |
| Network timeout | Checked | SocketTimeoutException |
| Database connection failed | Checked | SQLException |
| Null reference | Unchecked | NullPointerException |
| Invalid method argument | Unchecked | IllegalArgumentException |
| Array index out of bounds | Unchecked | ArrayIndexOutOfBoundsException |
| Division by zero | Unchecked | ArithmeticException |
| Invalid state | Unchecked | IllegalStateException |
- Recoverable + Out of control → Checked Exception
- Programming bug + Preventable → Unchecked Exception
Khi không chắc chắn, prefer unchecked exception (theo Spring Framework và nhiều modern frameworks).
Không catch Exception quá generic
❌ Anti-pattern: Catch quá rộng
// ❌ BAD: Catch Exception quá generic
try {
readFile("data.txt");
parseData();
saveToDatabase();
} catch (Exception e) {
// Không biết lỗi gì: File? Parse? Database?
System.out.println("Something went wrong");
}
Vấn đề:
- Không biết exception cụ thể là gì
- Xử lý giống nhau cho tất cả lỗi (không hợp lý)
- Có thể catch cả RuntimeException không mong muốn
- Khó debug
✅ Best Practice: Catch cụ thể
// ✅ GOOD: Catch exception cụ thể
try {
readFile("data.txt");
parseData();
saveToDatabase();
} catch (FileNotFoundException e) {
// Xử lý riêng cho file không tồn tại
System.err.println("File not found: " + e.getMessage());
useDefaultData();
} catch (ParseException e) {
// Xử lý riêng cho parse error
System.err.println("Invalid data format: " + e.getMessage());
askUserToFixData();
} catch (SQLException e) {
// Xử lý riêng cho database error
System.err.println("Database error: " + e.getMessage());
retryOrRollback();
} catch (Exception e) {
// Catch-all cho unexpected errors (cuối cùng)
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
Ngoại lệ: Top-level exception handler
// ✅ OK: Catch Exception ở top-level (logging, cleanup)
public static void main(String[] args) {
try {
runApplication();
} catch (Exception e) {
// Top-level handler: log và exit gracefully
logger.error("Application crashed", e);
cleanupResources();
System.exit(1);
}
}
// ❌ NEVER DO THIS
try {
// code
} catch (Throwable t) {
// Catch cả Error (OutOfMemoryError, StackOverflowError)
// Không thể recover từ Error!
}
Chỉ catch Exception, KHÔNG catch Throwable!
Không nuốt Exception (Empty catch block)
❌ Anti-pattern: Swallowing exceptions
// ❌ BAD: Empty catch block
try {
riskyOperation();
} catch (Exception e) {
// ❌ Nuốt exception, không làm gì
}
// ❌ BAD: Comment thay vì xử lý
try {
riskyOperation();
} catch (Exception e) {
// TODO: handle this later
}
// ❌ BAD: Chỉ print message
try {
riskyOperation();
} catch (Exception e) {
System.out.println("Error"); // ❌ Không đủ context
}
Vấn đề:
- Mất thông tin lỗi
- Khó debug (không biết lỗi xảy ra)
- Silent failure (người dùng không biết có lỗi)
- Có thể gây ra lỗi cascade sau đó
✅ Best Practice: Luôn xử lý exception
// ✅ GOOD: Log exception
try {
riskyOperation();
} catch (Exception e) {
logger.error("Failed to perform risky operation", e);
// Hoặc rethrow nếu không thể xử lý
throw new RuntimeException("Operation failed", e);
}
// ✅ GOOD: Log và fallback
try {
result = fetchFromCache();
} catch (CacheException e) {
logger.warn("Cache miss, fetching from database", e);
result = fetchFromDatabase(); // Fallback strategy
}
// ✅ GOOD: Log và notify user
try {
saveData(data);
} catch (IOException e) {
logger.error("Failed to save data", e);
showErrorMessage("Cannot save data. Please try again.");
}
Trường hợp hiếm hoi: Explicitly ignore exception
// ✅ OK: Explicitly ignore với comment giải thích
try {
// Attempt to close resource
resource.close();
} catch (IOException e) {
// Ignore: Resource already closed or cleanup is not critical
// We're in a shutdown hook, so we can't do much anyway
}
NEVER để empty catch block. Nếu thực sự muốn ignore, phải comment TẠI SAO ignore!
Logging Exceptions đúng cách
❌ Anti-patterns
// ❌ BAD: Chỉ log message, mất stack trace
try {
operation();
} catch (Exception e) {
logger.error(e.getMessage()); // ❌ Mất stack trace
}
// ❌ BAD: Log và rethrow (duplicate logging)
try {
operation();
} catch (Exception e) {
logger.error("Error", e);
throw e; // ❌ Caller cũng log → duplicate
}
// ❌ BAD: Log nhiều lần trong call chain
public void method1() {
try {
method2();
} catch (Exception e) {
logger.error("Error in method1", e); // Log lần 1
throw e;
}
}
public void method2() throws Exception {
try {
riskyOp();
} catch (Exception e) {
logger.error("Error in method2", e); // Log lần 2 (duplicate!)
throw e;
}
}
✅ Best Practices
// ✅ GOOD: Log exception object (bao gồm stack trace)
try {
operation();
} catch (Exception e) {
logger.error("Operation failed", e); // ✅ Log cả exception object
}
// ✅ GOOD: Log hoặc rethrow, KHÔNG cả hai
try {
operation();
} catch (IOException e) {
// Option 1: Log và handle (KHÔNG rethrow)
logger.error("I/O error, using fallback", e);
useFallback();
}
// Hoặc
try {
operation();
} catch (IOException e) {
// Option 2: Rethrow (KHÔNG log, để caller log)
throw new ServiceException("Service failed", e);
}
// ✅ GOOD: Log với context
try {
processUser(userId);
} catch (Exception e) {
logger.error("Failed to process user: userId={}", userId, e);
// Include relevant context (userId) in log
}
Log levels
// ✅ Sử dụng đúng log level
try {
result = cache.get(key);
} catch (CacheException e) {
logger.warn("Cache miss: {}", key, e); // WARN: Expected error, có fallback
result = database.get(key);
}
try {
saveData(data);
} catch (IOException e) {
logger.error("Failed to save data", e); // ERROR: Unexpected error
throw new RuntimeException("Cannot save", e);
}
try {
connectToBackup();
} catch (ConnectionException e) {
logger.debug("Backup server unavailable", e); // DEBUG: Low-priority info
}
Exception handling và Performance
❌ Anti-pattern: Exception for flow control
// ❌ BAD: Dùng exception để điều khiển luồng code
public int findIndex(String[] array, String value) {
try {
for (int i = 0; i < array.length; i++) {
if (array[i].equals(value)) {
throw new FoundException(i); // ❌ Terrible!
}
}
return -1;
} catch (FoundException e) {
return e.getIndex(); // ❌ Exception không phải để làm việc này
}
}
// ❌ BAD: Parse number với exception trong loop
for (String str : strings) {
try {
int num = Integer.parseInt(str); // ❌ Slow nếu nhiều invalid strings
total += num;
} catch (NumberFormatException e) {
// skip invalid
}
}
Vấn đề:
- Rất chậm: Creating exception tốn performance (populate stack trace)
- Khó đọc: Exception nên dùng cho exceptional cases
- Misleading: Làm người đọc nghĩ có lỗi
✅ Best Practice: Dùng control flow bình thường
// ✅ GOOD: Dùng return value
public int findIndex(String[] array, String value) {
for (int i = 0; i < array.length; i++) {
if (array[i].equals(value)) {
return i; // ✅ Simple return
}
}
return -1; // Not found
}
// ✅ GOOD: Validate trước khi parse
for (String str : strings) {
if (str.matches("\\d+")) { // ✅ Validate first
int num = Integer.parseInt(str); // Chỉ parse khi valid
total += num;
}
}
// Hoặc dùng utility method
for (String str : strings) {
Integer num = parseIntSafe(str); // ✅ Returns null nếu invalid
if (num != null) {
total += num;
}
}
private Integer parseIntSafe(String str) {
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
return null; // Return null thay vì throw
}
}
Performance comparison
// Benchmark: Exception vs Normal flow
public class ExceptionPerformance {
public static void main(String[] args) {
int iterations = 100_000;
// ❌ Với exception: ~1000ms
long start1 = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try {
throw new Exception();
} catch (Exception e) {
// catch
}
}
System.out.println("With exception: " + (System.currentTimeMillis() - start1) + "ms");
// ✅ Không exception: ~1ms
long start2 = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
// normal flow
}
System.out.println("Without exception: " + (System.currentTimeMillis() - start2) + "ms");
}
}
Exception là rất chậm. Chỉ dùng cho exceptional cases, không dùng cho normal flow control!
Defensive Programming vs Exception Handling
Defensive Programming: Prevent exceptions
// ✅ GOOD: Defensive programming - validate trước
public void processUser(User user) {
// Validate input
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
if (user.getEmail() == null || user.getEmail().isEmpty()) {
throw new IllegalArgumentException("User email is required");
}
// Now safe to use
sendEmail(user.getEmail());
}
// ✅ GOOD: Check null trước khi dereference
public int getLength(String text) {
if (text == null) {
return 0; // ✅ Handle null case
}
return text.length(); // ✅ Safe
}
// ✅ GOOD: Validate array index
public String getElement(String[] array, int index) {
if (index < 0 || index >= array.length) {
throw new IllegalArgumentException("Invalid index: " + index);
}
return array[index]; // ✅ Safe
}
Exception Handling: Handle unavoidable errors
// ✅ GOOD: Exception handling cho I/O (unavoidable)
public String readFile(String filename) {
// Không thể prevent file not found bằng code
try {
return Files.readString(Path.of(filename));
} catch (IOException e) {
logger.error("Failed to read file: {}", filename, e);
return ""; // Fallback
}
}
Kết hợp cả hai
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientFundsException {
// 1. Defensive programming: Validate input
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
// 2. Business logic check
if (amount > balance) {
// Throw custom checked exception
throw new InsufficientFundsException(
"Cannot withdraw " + amount + ", balance: " + balance
);
}
// 3. Perform operation
try {
balance -= amount;
saveToDatabase(); // Có thể throw SQLException
} catch (SQLException e) {
// 4. Exception handling: Handle unavoidable DB error
logger.error("Failed to save transaction", e);
balance += amount; // Rollback
throw new RuntimeException("Transaction failed", e);
}
}
}
Clean Code: Early Return và Guard Clauses
❌ Anti-pattern: Nested try-catch
// ❌ BAD: Deep nesting
public void processOrder(Order order) {
if (order != null) {
if (order.getItems() != null) {
if (!order.getItems().isEmpty()) {
try {
validateOrder(order);
try {
processPayment(order);
try {
shipOrder(order);
} catch (ShippingException e) {
// handle
}
} catch (PaymentException e) {
// handle
}
} catch (ValidationException e) {
// handle
}
}
}
}
}
✅ Best Practice: Guard clauses + Early return
// ✅ GOOD: Guard clauses, flat structure
public void processOrder(Order order) throws OrderProcessingException {
// Guard clause 1: Validate order
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
// Guard clause 2: Validate items
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
// Happy path - flat code
try {
validateOrder(order);
} catch (ValidationException e) {
logger.error("Order validation failed: {}", order.getId(), e);
throw new OrderProcessingException("Invalid order", e);
}
try {
processPayment(order);
} catch (PaymentException e) {
logger.error("Payment failed: {}", order.getId(), e);
throw new OrderProcessingException("Payment failed", e);
}
try {
shipOrder(order);
} catch (ShippingException e) {
logger.error("Shipping failed: {}", order.getId(), e);
refundPayment(order); // Compensating transaction
throw new OrderProcessingException("Shipping failed", e);
}
}
Fail Fast Principle
// ✅ GOOD: Fail fast - validate tất cả upfront
public void createUser(String username, String email, int age) {
// Fail fast: Validate all inputs first
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("Username is required");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age");
}
// All validations passed, proceed with business logic
User user = new User(username, email, age);
saveToDatabase(user);
}
Bài tập: File Validator Mini Project
Tạo một hệ thống validate file với các requirements sau:
Requirements
-
Custom Exceptions:
FileValidationException(base checked exception)FileTooLargeException(max 10MB)UnsupportedFileTypeException(chỉ accept: jpg, png, pdf, txt)EmptyFileException(file size = 0)
-
Validator class:
validateFile(File file): Validate file theo các rules trênvalidateFiles(List<File> files): Validate nhiều files, collect tất cả errors
-
Best practices:
- Defensive programming: null checks, guard clauses
- Exception chaining
- Proper logging
- Clean code: early return
Solution Template
// ============= Custom Exceptions =============
public class FileValidationException extends Exception {
private final String filename;
public FileValidationException(String message, String filename) {
super(message);
this.filename = filename;
}
public FileValidationException(String message, String filename, Throwable cause) {
super(message, cause);
this.filename = filename;
}
public String getFilename() {
return filename;
}
}
public class FileTooLargeException extends FileValidationException {
private static final long MAX_SIZE = 10 * 1024 * 1024; // 10MB
private final long fileSize;
public FileTooLargeException(String filename, long fileSize) {
super(
String.format("File too large: %s (%.2f MB > %.2f MB)",
filename, fileSize / 1024.0 / 1024.0, MAX_SIZE / 1024.0 / 1024.0),
filename
);
this.fileSize = fileSize;
}
public long getFileSize() { return fileSize; }
public long getMaxSize() { return MAX_SIZE; }
}
public class UnsupportedFileTypeException extends FileValidationException {
private static final List<String> SUPPORTED_TYPES =
Arrays.asList("jpg", "jpeg", "png", "pdf", "txt");
private final String fileType;
public UnsupportedFileTypeException(String filename, String fileType) {
super(
String.format("Unsupported file type: %s (expected: %s)",
fileType, SUPPORTED_TYPES),
filename
);
this.fileType = fileType;
}
public String getFileType() { return fileType; }
public List<String> getSupportedTypes() { return SUPPORTED_TYPES; }
}
public class EmptyFileException extends FileValidationException {
public EmptyFileException(String filename) {
super("File is empty: " + filename, filename);
}
}
// ============= Validator =============
public class FileValidator {
private static final Logger logger = LoggerFactory.getLogger(FileValidator.class);
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
private static final List<String> SUPPORTED_EXTENSIONS =
Arrays.asList("jpg", "jpeg", "png", "pdf", "txt");
/**
* Validate single file
*/
public void validateFile(File file) throws FileValidationException {
// Guard clause 1: Null check
if (file == null) {
throw new IllegalArgumentException("File cannot be null");
}
// Guard clause 2: File exists
if (!file.exists()) {
throw new FileValidationException("File not found", file.getName());
}
// Guard clause 3: Is file (not directory)
if (!file.isFile()) {
throw new FileValidationException("Not a file", file.getName());
}
// Validation 1: Empty file
if (file.length() == 0) {
throw new EmptyFileException(file.getName());
}
// Validation 2: File size
if (file.length() > MAX_FILE_SIZE) {
throw new FileTooLargeException(file.getName(), file.length());
}
// Validation 3: File type
String extension = getFileExtension(file);
if (!SUPPORTED_EXTENSIONS.contains(extension.toLowerCase())) {
throw new UnsupportedFileTypeException(file.getName(), extension);
}
logger.info("File validated successfully: {}", file.getName());
}
/**
* Validate multiple files, collect all errors
*/
public Map<File, FileValidationException> validateFiles(List<File> files) {
// Guard clause
if (files == null || files.isEmpty()) {
throw new IllegalArgumentException("File list cannot be null or empty");
}
Map<File, FileValidationException> errors = new HashMap<>();
for (File file : files) {
try {
validateFile(file);
} catch (FileValidationException e) {
// Collect error, continue validating other files
errors.put(file, e);
logger.warn("File validation failed: {}", file.getName(), e);
}
}
return errors;
}
private String getFileExtension(File file) {
String name = file.getName();
int lastDot = name.lastIndexOf('.');
if (lastDot == -1) {
return ""; // No extension
}
return name.substring(lastDot + 1);
}
}
// ============= Usage Example =============
public class FileValidatorDemo {
public static void main(String[] args) {
FileValidator validator = new FileValidator();
// Test single file
try {
File file = new File("document.pdf");
validator.validateFile(file);
System.out.println("✅ File is valid");
} catch (FileTooLargeException e) {
System.err.printf("❌ File too large: %.2f MB (max: %.2f MB)%n",
e.getFileSize() / 1024.0 / 1024.0,
e.getMaxSize() / 1024.0 / 1024.0);
} catch (UnsupportedFileTypeException e) {
System.err.printf("❌ Unsupported type: %s (supported: %s)%n",
e.getFileType(),
e.getSupportedTypes());
} catch (EmptyFileException e) {
System.err.println("❌ File is empty");
} catch (FileValidationException e) {
System.err.println("❌ Validation error: " + e.getMessage());
}
// Test multiple files
List<File> files = Arrays.asList(
new File("image.jpg"),
new File("large.pdf"),
new File("document.docx")
);
Map<File, FileValidationException> errors = validator.validateFiles(files);
if (errors.isEmpty()) {
System.out.println("✅ All files are valid");
} else {
System.out.println("❌ " + errors.size() + " file(s) failed validation:");
errors.forEach((file, exception) -> {
System.out.println(" - " + file.getName() + ": " + exception.getMessage());
});
}
}
}
Exception Handling trong Streams
Lambda expressions trong Java Streams không cho phép throw checked exceptions trực tiếp. Đây là vấn đề thường gặp và hay xuất hiện trong OCP exam.
Vấn đề
// ❌ COMPILE ERROR: Unhandled checked exception
List<String> files = List.of("a.txt", "b.txt", "c.txt");
files.stream()
.map(f -> new FileReader(f)) // ❌ FileReader throws FileNotFoundException
.collect(Collectors.toList());
Giải pháp 1: Wrap trong try-catch bên trong lambda
// ✅ Wrap checked exception thành unchecked
files.stream()
.map(f -> {
try {
return new FileReader(f);
} catch (FileNotFoundException e) {
throw new UncheckedIOException(e); // Wrap thành unchecked
}
})
.collect(Collectors.toList());
Giải pháp 2: Utility method unchecked()
@FunctionalInterface
interface CheckedFunction<T, R> {
R apply(T t) throws Exception;
}
// Utility: chuyển checked function thành unchecked
static <T, R> Function<T, R> unchecked(CheckedFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
// ✅ Sử dụng: code gọn hơn nhiều
files.stream()
.map(unchecked(f -> new FileReader(f)))
.collect(Collectors.toList());
Giải pháp 3: Dùng Optional để skip lỗi
// ✅ Skip các phần tử lỗi thay vì crash
List<Integer> numbers = List.of("1", "abc", "3", "xyz", "5");
List<Integer> parsed = numbers.stream()
.map(s -> {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.<Integer>empty();
}
})
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
// Result: [1, 3, 5]
Optional vs Exception
Khi một method "không tìm thấy kết quả", bạn có 3 lựa chọn: return null, throw exception, hoặc return Optional. Khi nào dùng cái nào?
So sánh
| Cách | Khi nào dùng | Ví dụ |
|---|---|---|
| Exception | "Không tìm thấy" là lỗi thật sự — caller không nên tiếp tục | findActiveSubscription() — user phải có subscription |
| Optional | "Không tìm thấy" là kết quả hợp lệ — caller tự quyết tiếp | findByEmail() — user có thể chưa đăng ký |
| null | Tránh dùng — dễ gây NullPointerException | — |
Ví dụ thực tế
// ✅ Optional: "không tìm thấy" là bình thường
public Optional<User> findById(String id) {
User user = database.get(id);
return Optional.ofNullable(user);
}
// Caller tự quyết
User user = userService.findById("123")
.orElseThrow(() -> new UserNotFoundException("User 123 not found"));
// Hoặc dùng default
String email = userService.findById("123")
.map(User::getEmail)
.orElse("[email protected]");
// ✅ Exception: "không tìm thấy" là lỗi cần xử lý
public User getActiveUser(String id) throws UserNotFoundException {
User user = database.get(id);
if (user == null || !user.isActive()) {
throw new UserNotFoundException("Active user not found: " + id);
}
return user;
}
- Method tên
find.../get...OrNull→ return Optional - Method tên
get.../load...→ throw Exception nếu không tìm thấy - Không bao giờ return
Optional.of(null)— dùngOptional.empty()
ExceptionInInitializerError
Khi exception xảy ra trong static initializer hoặc static field initialization, JVM wrap nó trong ExceptionInInitializerError. Lần truy cập tiếp theo vào class đó sẽ throw NoClassDefFoundError.
class BadInit {
static int value = 10 / 0; // ArithmeticException trong static init
}
public class Main {
public static void main(String[] args) {
try {
new BadInit(); // Lần 1: ExceptionInInitializerError
} catch (ExceptionInInitializerError e) {
System.out.println("Lần 1: " + e.getCause()); // ArithmeticException
}
try {
new BadInit(); // Lần 2: NoClassDefFoundError (class bị đánh dấu failed)
} catch (NoClassDefFoundError e) {
System.out.println("Lần 2: " + e.getMessage());
}
}
}
Lý do: JVM chỉ thử initialize class 1 lần. Nếu fail, class bị đánh dấu "unusable" vĩnh viễn trong ClassLoader đó.
Tóm tắt Best Practices
| Best Practice | ✅ Do | ❌ Don't |
|---|---|---|
| Exception type | Catch cụ thể, specific → general | catch (Exception e) quá sớm |
| Empty catch | Luôn log hoặc xử lý | Empty catch block |
| Flow control | Dùng if/else, return | Dùng exception cho normal flow |
| Logging | Log exception object + context | Chỉ log message |
| Checked vs Unchecked | Checked cho recoverable, Unchecked cho bugs | Dùng sai loại |
| Guard clauses | Validate early, fail fast | Deep nesting |
| Exception chaining | Preserve cause với new Ex(msg, cause) | Throw mới mà mất cause |
| Performance | Tránh exception trong loop/hot path | Exception for flow control |
| Streams | Wrap checked thành unchecked trong lambda | Throw checked trong lambda |
| Optional | Dùng cho "not found" hợp lệ | Return null |
- Fail Fast: Validate inputs sớm nhất có thể
- Specific Exceptions: Catch và throw exception cụ thể
- Always Handle: Không bao giờ nuốt exception
- Log Properly: Log exception object với context
- Clean Code: Guard clauses, early return, flat structure
- Performance: Exception chỉ cho exceptional cases
- Defensive: Validate, check null, prevent exceptions
Bài tập tổng hợp
- Review Code: Tìm tất cả anti-patterns trong code sau và fix:
public void processData(String filename) {
try {
File file = new File(filename);
String data = new String(Files.readAllBytes(file.toPath()));
int value = Integer.parseInt(data);
int result = 100 / value;
System.out.println(result);
} catch (Exception e) {
// TODO: handle later
}
}
- Refactor: Cải thiện exception handling trong method sau:
public List<User> getUsers(List<String> ids) {
List<User> users = new ArrayList<>();
for (String id : ids) {
try {
users.add(database.findUser(id));
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
return users;
}
-
Design: Thiết kế exception hierarchy cho E-commerce Order Processing với các exceptions phù hợp (checked vs unchecked).
-
Performance: Đo performance của code sử dụng exception vs code không dùng exception cho việc parse 100,000 strings.
-
Complete Project: Hoàn thiện File Validator project với:
- Unit tests cho tất cả exception cases
- Logging với SLF4J
- Builder pattern cho custom exceptions
- CLI interface để validate files từ command line
Chúc mừng bạn đã hoàn thành Module 5: Exception Handling! Áp dụng những best practices này vào code thực tế để tạo ra ứng dụng robust và maintainable.
Module tiếp theo: Collections Framework - Arrays, Lists, Sets, Maps!