Backend

99 Ngày Java — Ngày 41: Exception tổng quan

SSite Admin
6 tháng 09, 2026 7 phút đọc 0 lượt xem
99 Ngày Java — Ngày 41: Exception tổng quan

Bắt đầu một giai đoạn mới của series: xử lý lỗi. Suốt bốn mươi ngày ta viết code cho trường hợp mọi thứ suôn sẻ — hôm nay nói về lúc không suôn sẻ. Ba thứ cần nắm ngay: cây thừa kế Throwable và vì sao nó quyết định cách hành xử, ranh giới checked so với unchecked cùng câu hỏi duy nhất để phân loại, và cách đọc stack trace — kỹ năng gỡ lỗi giá trị nhất mà ít ai được dạy tử tế.

Sketchnote Ngày 41: Exception tổng quan — cây thừa kế Throwable với Error, checked và unchecked, câu hỏi phân loại, cách đọc stack trace từ dòng trên cùng và Caused by, cùng bốn kiểu bắt exception sai

Cây Throwable — biết cây là biết cách hành xử

Toàn bộ hệ thống lỗi của Java treo trên một gốc duy nhất là Throwable, và nó chia làm hai nhánh mang ý nghĩa hoàn toàn khác nhau. Nhớ được cấu trúc này thì bạn tự biết cái nào nên bắt, cái nào nên để nó bay lên.

// Cả cây thừa kế chỉ có một gốc, và biết cây này là biết cách hành xử:
//
//                    Throwable
//                   /         \
//              Error           Exception
//         (JVM hỏng)          /         \
//                    RuntimeException   (mọi Exception khác)
//                    = UNCHECKED        = CHECKED

// Error — JVM đang chết, ĐỪNG bắt:
//   OutOfMemoryError, StackOverflowError, NoClassDefFoundError
// Bắt được nhưng bắt để làm gì? Bộ nhớ đã hết thì code xử lý cũng không chạy nổi.

// Checked — trình biên dịch BẮT BUỘC xử lý hoặc khai throws.
// Ý nghĩa: "chuyện này nằm ngoài tầm kiểm soát của code, hãy có kế hoạch":
//   IOException, SQLException, InterruptedException

// Unchecked (RuntimeException) — không bắt buộc gì cả.
// Ý nghĩa: "đây là LỖI LẬP TRÌNH, sửa code đi chứ đừng bắt":
//   NullPointerException, IllegalArgumentException,
//   IndexOutOfBoundsException, ClassCastException

// Câu hỏi phân loại chỉ có một: NGƯỜI GỌI CÓ THỂ LÀM GÌ ĐÓ để phục hồi không?
//   Có  → checked  (file không tồn tại → hỏi lại đường dẫn)
//   Không → unchecked (id null → đó là bug, sửa code)
  • Error nghĩa là máy ảo đang chết — bắt nó gần như không có ích, vì code xử lý của bạn cũng không còn tài nguyên để chạy.

  • Checked là lời nhắc của trình biên dịch: chuyện này nằm ngoài tầm kiểm soát của code, hãy có kế hoạch.

  • Unchecked nghĩa là lỗi lập trình: đừng bắt, hãy sửa. Bắt NullPointerException để chạy tiếp là đang giấu bug.

  • Câu hỏi phân loại chỉ có một: người gọi có thể làm gì đó để phục hồi không? Có thì checked, không thì unchecked.

Đọc stack trace cho đúng

Rất nhiều người nhìn stack trace rồi hoảng vì nó dài. Nhưng nó có cấu trúc rất rõ, và bạn chỉ cần ba mẩu thông tin: nổ ở đâu, vì sao, và nguyên nhân gốc là gì.

// Đọc stack trace: kỹ năng gỡ lỗi giá trị nhất, mà ít ai được dạy tử tế.
Exception in thread "main" java.lang.NullPointerException:
        Cannot invoke "String.length()" because "ten" is null      ← ①
    at vn.motdev.DonHangService.tinhTong(DonHangService.java:42)   ← ②
    at vn.motdev.DonHangController.tao(DonHangController.java:18)
    at vn.motdev.Main.main(Main.java:9)                            ← ③
Caused by: java.sql.SQLException: connection timed out             ← ④
    at ...
    ... 12 more                                                    ← ⑤

// ① LOẠI + THÔNG ĐIỆP. Từ Java 14, NullPointerException nói thẳng biến nào null
//    (helpful NPE) — trước đó chỉ có mỗi chữ "NullPointerException" trơ trọi.
// ② DÒNG TRÊN CÙNG là nơi NỔ. Đọc từ đây, đừng đọc từ dưới lên.
// ③ Càng xuống dưới càng xa nguyên nhân — đó chỉ là đường đi tới chỗ nổ.
// ④ "Caused by" là VÀNG: lỗi GỐC nằm ở đây. Có nhiều tầng thì đọc CÁI DƯỚI CÙNG.
// ⑤ "... 12 more" chỉ là phần trùng với stack phía trên, đã lược bớt.

// MẸO ĐỌC NHANH: bỏ qua mọi dòng của thư viện, tìm dòng ĐẦU TIÊN có package
// của chính bạn. Chín trên mười lần, bug nằm ở đó.
  • Đọc từ trên xuống: dòng đầu là nơi nổ, càng xuống dưới càng chỉ là đường đi tới đó.

  • Caused by là phần giá trị nhất — và khi có nhiều tầng thì cái dưới cùng mới là nguyên nhân thật.

  • Mẹo nhanh: bỏ qua mọi dòng của framework, tìm dòng đầu tiên thuộc package của chính bạn.

  • Từ Java 14, helpful NPE nói thẳng biến nào đang null — một trong những cải tiến đời sống lớn nhất của ngôn ngữ.

Bốn kiểu bắt exception khiến người khác khổ

Phần lớn nỗi đau khi gỡ lỗi không đến từ exception, mà đến từ cách người ta xử lý nó. Bốn kiểu dưới đây là bốn thứ hay gặp nhất, và cả bốn đều biến một lỗi rõ ràng thành một bí ẩn.

// Bốn kiểu bắt exception khiến đồng nghiệp mất cả buổi để gỡ:

// 1. NUỐT LỖI — tội nặng nhất trong nghề:
try { xuLy(); } catch (Exception e) { }         // ❌ lỗi biến mất không dấu vết
try { xuLy(); } catch (Exception e) { e.printStackTrace(); }  // ❌ gần như cũng vậy
// printStackTrace ghi ra luồng lỗi chuẩn, không vào hệ thống log, không có ngữ cảnh,
// không cảnh báo ai. Ở production nghĩa là không ai biết.

// 2. BẮT QUÁ RỘNG:
try { docFile(); } catch (Exception e) { }      // ❌ nuốt luôn cả bug của chính bạn
try { docFile(); } catch (IOException e) { }    // ✅ bắt đúng thứ mình xử lý được

// 3. MẤT NGUYÊN NHÂN GỐC khi bọc lại:
throw new AppException("Không đọc được file");        // ❌ mất sạch stack trace gốc
throw new AppException("Không đọc được file", e);     // ✅ LUÔN truyền cause vào

// 4. DÙNG EXCEPTION LÀM LUỒNG ĐIỀU KHIỂN:
try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; }
// Chạy đúng, nhưng exception ĐẮT (phải dựng stack trace) và che mất ý định.
// Kiểm tra trước vẫn rõ ràng và nhanh hơn nhiều.

// LÀM ĐÚNG: bắt thứ mình xử lý được, log KÈM NGỮ CẢNH, còn lại thì ném lên trên.
catch (IOException e) {
    log.error("Không đọc được cấu hình từ {}", duongDan, e);   // e là tham số CUỐI
    throw new CauHinhException("Không đọc được cấu hình", e);
}
  • Nuốt lỗi là tội nặng nhất: catch rỗng biến một lỗi rõ ràng thành một hành vi sai bí ẩn ở nơi khác.

  • e.printStackTrace() gần như cũng là nuốt lỗi: nó không vào hệ thống log, không có ngữ cảnh, không báo cho ai.

  • Luôn truyền cause khi bọc lại — quên nó là vứt đi toàn bộ thông tin về nơi thật sự hỏng.

  • Khi log, exception là tham số cuối cùng của SLF4J: log.error("...{}", x, e) thì mới in ra stack trace.

Bài tập nhỏ

  • Viết một chương trình ném NullPointerException trên Java 21 và đọc thông điệp helpful NPE — so với việc phải tự đoán ngày xưa.

  • Tạo chuỗi ba tầng: SQLException bị bọc bởi DataAccessException rồi bọc bởi exception của bạn — in ra và tìm nguyên nhân gốc.

  • Bọc lại một exception mà quên truyền cause, rồi so stack trace với bản có truyền cause.

  • Đo thời gian parseInt trong try-catch so với kiểm tra bằng regex trên một triệu chuỗi không hợp lệ.

  • Tìm trong dự án của bạn mọi catch rỗng hoặc chỉ có printStackTrace — và sửa ít nhất một chỗ cho tử tế.

Kết lại

Bốn ý gói lại hôm nay: cây Throwable chia làm Error (đừng bắt), checked (ngoài tầm kiểm soát, phải có kế hoạch) và unchecked (lỗi lập trình, hãy sửa); câu hỏi phân loại duy nhất là người gọi có phục hồi được không; đọc stack trace thì từ trên xuống và tìm Caused by dưới cùng; còn khi bắt thì đừng nuốt, đừng bắt quá rộng, luôn truyền cause, và đừng dùng exception làm luồng điều khiển. Ngày 42 ta đi sâu vào cú pháp xử lý: try-catch-finally đúng cách — multi-catch, những cái bẫy chết người của finally, và khi nào nên bọc lại exception. Hẹn gặp lại!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 41: Kiến trúc Spring Security

Security là chuỗi filter đứng trước controller chứ không nằm trong nó, SecurityContext giữ Authentication trong ThreadLocal, phân biệt authentication 401 với authorization 403 — và bẫy hasRole so với hasAuthority.

6 thg 9, 20266 phút1
99 Ngày Spring — Ngày 40: Tổng kết tối ưu JPA

Checklist bảy vấn đề theo thứ tự đáng sửa trước: N+1 query và ba cách chữa, projection, keyset pagination, index khóa ngoại, transaction gọn, batch ghi — và nguyên tắc đo trước, cache cuối cùng.

5 thg 9, 20267 phút5
99 Ngày Java — Ngày 40: Wildcard & PECS

Vì sao List<Integer> không truyền được vào List<Number>, ? extends chỉ đọc và ? super chỉ ghi, quy tắc PECS giải thích luôn chữ ký của nửa bộ Collections — cùng giới hạn và lúc không nên dùng wildcard.

5 thg 9, 20268 phút5