99 Ngày Java — Ngày 42: try-catch-finally đúng cách
Hôm qua ta xem bản đồ của exception; hôm nay đi vào cú pháp xử lý. Ba thứ đáng học kỹ: thứ tự catch là luật chứ không phải sở thích, multi-catch cho những trường hợp xử lý giống nhau, và quan trọng nhất — ba cái bẫy của finally, trong đó có một cái làm exception biến mất hoàn toàn mà không để lại dấu vết nào.

Thứ tự catch và multi-catch
Nhiều người viết catch theo thứ tự nghĩ ra, rồi ngạc nhiên khi trình biên dịch từ chối. Thật ra luật rất đơn giản và hợp lý: con trước, cha sau — vì nếu cha đứng trước thì nó bắt hết, và khối phía sau vĩnh viễn không có cơ hội chạy.
// Nhiều catch cho nhiều loại lỗi — nhưng THỨ TỰ có luật:
try {
doc(duongDan);
} catch (FileNotFoundException e) { // CON phải đứng TRƯỚC
log.warn("Không thấy file {}", duongDan);
} catch (IOException e) { // CHA đứng sau
log.error("Lỗi đọc file {}", duongDan, e);
}
// Đảo ngược lại thì KHÔNG BIÊN DỊCH ĐƯỢC: IOException đã bắt hết,
// khối FileNotFoundException phía sau vĩnh viễn không bao giờ chạy tới.
// MULTI-CATCH (Java 7+) — khi cách xử lý GIỐNG NHAU:
try {
xuLy();
} catch (IOException | SQLException e) { // một khối cho hai loại
throw new AppException("Không truy cập được dữ liệu", e);
}
// Biến e ở đây là ngầm final — không gán lại được. Và kiểu của nó là
// tổ tiên chung gần nhất, nên chỉ gọi được method mà cả hai đều có.
// ĐỪNG bắt rồi phân loại bằng instanceof — đó là quay lại thời chưa có multi-catch:
catch (Exception e) {
if (e instanceof IOException) { ... } // ❌ trình biên dịch làm hộ được mà
}Con trước cha sau là luật của trình biên dịch, không phải quy ước phong cách — viết ngược là lỗi biên dịch.
multi-catchdành cho lúc cách xử lý giống hệt nhau; biến bắt được là ngầmfinalvà mang kiểu tổ tiên chung.Bắt rộng rồi phân loại bằng
instanceoflà bỏ phí việc mà trình biên dịch làm hộ được — và dễ sót nhánh.
Ba cái bẫy của finally
finally có một lời hứa rất mạnh: nó luôn chạy. Chính lời hứa đó tạo ra ba tình huống mà hành vi của chương trình đi ngược hoàn toàn trực giác — và cái đầu tiên là một trong những cách âm thầm nhất để đánh mất một lỗi.
// finally LUÔN chạy — kể cả khi có return, break, hay exception khác.
// Chính vì "luôn chạy" mà nó sinh ra ba cái bẫy khó chịu nhất Java.
// BẪY 1 — return trong finally NUỐT MẤT exception:
static int nguyHiem() {
try {
throw new RuntimeException("lỗi thật");
} finally {
return 42; // ❌ exception BIẾN MẤT, hàm trả về 42!
}
}
// Không lỗi, không log, không dấu vết. Đây là lý do IDE luôn cảnh báo
// "finally block does not complete normally".
// BẪY 2 — finally GHI ĐÈ giá trị trả về:
static int demSo() {
int x = 1;
try { return x; } // ghi nhớ giá trị 1 để trả về
finally { x = 99; } // đổi x, nhưng KHÔNG đổi giá trị đã ghi nhớ
} // → vẫn trả về 1 (dễ nhầm nhất khi đọc lướt)
// BẪY 3 — exception TRONG finally che mất exception GỐC:
try {
throw new RuntimeException("lỗi gốc"); // ← cái bạn CẦN thấy
} finally {
dong(); // nếu dong() cũng ném → lỗi gốc MẤT
}
// Đây chính là lý do try-with-resources (Ngày 44) ra đời: nó giữ lại
// lỗi gốc và đính lỗi lúc đóng vào phần "Suppressed".
// LUẬT NGÓN TAY CÁI: trong finally CHỈ dọn dẹp. Không return, không throw.return trong finally nuốt mất exception: lỗi biến mất sạch, hàm trả về bình thường như chưa có chuyện gì. Đây là bẫy nguy hiểm nhất trong ba cái.
Sửa biến trong
finallykhông đổi giá trị đã được ghi nhớ để trả về — đọc lướt rất dễ hiểu nhầm.Exception ném ra từ trong
finallysẽ che mất exception gốc — chính vấn đề màtry-with-resourcescủa Ngày 44 sinh ra để giải quyết.Luật ngón tay cái: trong
finallychỉ dọn dẹp — khôngreturn, khôngthrow.
Ném lại hay bọc lại
Khi bắt được một exception mà tầng của bạn không xử lý nổi, có hai lựa chọn, và chọn cái nào là một quyết định thiết kế: ném lại nguyên vẹn để tầng trên thấy đúng thứ đã xảy ra, hay bọc lại thành lỗi mang ngôn ngữ của tầng cao hơn.
// Ném lại hay bọc lại? Câu hỏi thiết kế, không phải cú pháp.
// NÉM LẠI NGUYÊN VẸN — khi tầng này không xử lý được, chỉ muốn ghi log:
catch (IOException e) {
log.error("Không đọc được {}", duongDan, e);
throw e; // giữ nguyên kiểu và stack trace
}
// BỌC LẠI — khi muốn đổi sang ngôn ngữ của tầng cao hơn:
catch (SQLException e) {
throw new KhoDuLieuException("Không lưu được đơn hàng " + id, e);
}
// Vì sao bọc? Để tầng trên không phải biết bên dưới là SQL hay là file hay là API.
// Đây chính là cách Spring biến SQLException thành DataAccessException.
// QUY TẮC VÀNG: LUÔN truyền cause. Quên nó là vứt đi nơi lỗi thật sự xảy ra —
// và người sửa bug lúc 2 giờ sáng sẽ chỉ thấy "Không lưu được đơn hàng 42",
// không có gì cho biết vì sao.
// InterruptedException — trường hợp đặc biệt hay bị làm sai:
try { Thread.sleep(1000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ❗ KHÔI PHỤC cờ ngắt
throw new AppException("Bị ngắt khi đang chờ", e);
}
// Bắt rồi bỏ qua là xóa mất tín hiệu "hãy dừng lại" mà ai đó vừa gửi cho luồng này.
// Và đừng quên: catch(Exception) cũng nuốt luôn cả RuntimeException của chính bạn.
// Bắt hẹp nhất có thể, để bug của mình vẫn nổ to và rõ.Bọc lại để tầng trên không phải biết bên dưới là gì — đây chính là cách Spring biến
SQLExceptionthànhDataAccessException.Luôn truyền cause, không có ngoại lệ. Quên nó là biến một stack trace đầy đủ thành một câu thông báo cụt lủn.
InterruptedExceptionphải khôi phục cờ ngắt bằngThread.currentThread().interrupt()— bắt rồi bỏ qua là xóa mất tín hiệu dừng.
Bài tập nhỏ
Viết hàm ném exception trong
tryvàreturntrongfinally— chạy thử và nhìn exception biến mất.Viết
catch (IOException e)đứng trướccatch (FileNotFoundException e)và đọc chính xác lỗi biên dịch.Trong
finally, gọi một method cũng ném exception — quan sát lỗi gốc bị che mất thế nào.Bọc một
SQLExceptionmà cố tình quên cause, rồi so hai stack trace cạnh nhau.Viết vòng lặp gọi
Thread.sleep, bắtInterruptedExceptionrồi bỏ qua, và thử ngắt luồng đó — xem nó vẫn chạy tiếp.
Kết lại
Bốn ý gói lại hôm nay: thứ tự catch là con trước cha sau, và multi-catch dành cho những nhánh xử lý giống nhau; finally luôn chạy nên đừng bao giờ return hay throw trong đó — return ở đây nuốt sạch exception; ném lại hay bọc lại là quyết định thiết kế, và dù chọn cách nào thì luôn truyền cause; còn InterruptedException thì phải khôi phục cờ ngắt chứ không được nuốt. Ngày 43 ta chuyển sang phía thiết kế: custom exception — khi nào đáng tạo lớp exception riêng, đặt tên và viết message thế nào cho người đọc log hiểu được. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


