Java OO & Functional/try / catch / finally — cây Throwable và cơ chế throw
10/38
Bài 10 / 38~20 phútException HandlingMiễn phí lượt xem

try / catch / finally — cây Throwable và cơ chế throw

Cây Throwable (Error, Exception, RuntimeException), try/catch/finally, throw, multi-catch Java 7+. Anti-pattern nuốt exception. Phần cơ chế sâu (stack trace, cause chain, exception table) ở bài 2.

Chương trình Java gặp sự cố — file không tồn tại, chia cho 0, array index âm, null dereference — lúc đó phải làm gì? Ngôn ngữ cũ C trả error code: -1 nghĩa lỗi, 0 ok. Caller phải check return sau mọi lời gọi. Bỏ quên 1 check → bug silent. Code 80% là if-check, 20% là logic thật.

Java chọn cơ chế khác — exception: khi lỗi, method ném (throw) object mô tả lỗi, JVM tự unwind call stack cho đến khi tìm được catch xử lý. Caller không phải check return — exception bắt buộc được xử lý ở đâu đó (hoặc crash program).

Bài này đi qua cú pháp và rule cơ bản:

  • Cây Throwable hierarchy — chọn exception nào khi throw.
  • try/catch/finally semantic.
  • throw chủ động và chọn exception class đúng.
  • Multi-catch Java 7+.
  • Anti-pattern: nuốt exception.

Cơ chế sâu đi vào hai bài kế tiếp: bài 2 dạy đọc stack trace + cause chain; bài 3 mổ xẻ exception table bytecode + cost của exception — để hiểu vì sao throw rẻ và cách JVM handle exception runtime.

1. Analogy — báo cháy qua tầng

Toà nhà 10 tầng, tầng 7 phát cháy. Nhân viên tầng 7 có 2 lựa chọn:

  1. Tự xử: có bình cứu hoả → dùng → tiếp tục làm việc. Tương đương catch + continue.
  2. Báo lên: không xử được → kích chuông báo động → tầng trên biết, đến giải quyết. Tương đương throw propagate.

Dù chọn gì, 1 việc luôn làm: đóng sổ theo dõi (ghi end-of-day). Đó là finally — luôn chạy dù xử lý được hay không.

Khác biệt quan trọng với error code: exception bắt buộc được xử lý ở đâu đó — không thể "quên check". Caller bỏ qua → exception propagate lên main → JVM in stack trace và crash.

Đời thườngJava
Sự cố phát sinhthrow new Exception(...)
Tự xửcatch (Exception e) { ... }
Báo lên tầng trênKhông catch, propagate
Đóng sổ (luôn làm)finally { ... }

Cơ chế Unwind Stack (Cuộn ngược Call Stack) của JVM

Khi một ngoại lệ được ném ra mà không được xử lý ngay tại phương thức hiện tại, JVM sẽ thực hiện quá trình cuộn ngược Call Stack (gọi là Stack Unwinding). JVM sẽ đi ngược từ phương thức con lên các phương thức cha đã gọi nó để tìm kiếm một khối catch phù hợp.

Bốn khung stack tại đúng thời điểm level3() ném exception, kèm số phận của từng khung:

Bốn khung stack xếp dọc, mũi tên đi ngược từ level3 lên level2 rồi tới level1: level3 ném IOException, level2 không có catch nên bị pop và mất biến cục bộ, level1 khớp kiểu nên bắt được, main không bao giờ nhìn thấy exception

Thứ tự tìm ngược thứ tự gọi và dừng ở khung đầu tiên khớp kiểu. Mỗi khung bị pop mang theo toàn bộ biến cục bộ của nó — nên thông tin cần cho debug phải nằm trong chính exception, không phải trong biến của khung đã chết.

2. Cây Throwable — hierarchy exception

Đây là bức tranh bạn phải thuộc — hiểu cây này giúp quyết định catch gì, khi nào:

Throwable                            (goc cua tat ca, co stack trace)
├── Error                            (JVM loi nang - KHONG catch)
│   ├── OutOfMemoryError            (heap het)
│   ├── StackOverflowError          (recursion vo tan)
│   ├── NoClassDefFoundError        (classpath sai)
│   └── ...
│
└── Exception                        (loi ung dung)
    ├── RuntimeException            (unchecked - compiler KHONG ep)
    │   ├── NullPointerException    (NPE)
    │   ├── IllegalArgumentException
    │   ├── IllegalStateException
    │   ├── IndexOutOfBoundsException
    │   ├── ArithmeticException     (chia 0)
    │   ├── ClassCastException
    │   ├── NumberFormatException
    │   └── ...
    │
    └── (checked - compiler EP khai throws hoac catch)
        ├── IOException
        │   ├── FileNotFoundException
        │   └── EOFException
        ├── SQLException
        ├── ClassNotFoundException
        ├── InterruptedException
        └── ...

Error — lỗi JVM nặng

  • JVM gặp tình huống không chạy tiếp được: hết heap, stack overflow, class file corrupt.
  • Không catch. Nếu catch được cũng không nên xử lý — JVM đã ở trạng thái không ổn định.
  • OutOfMemoryError về lý thuyết bắt được, nhưng catch không giúp gì — thường heap đã cạn.

Exception — lỗi ứng dụng

Chia 2 nhóm dựa trên compiler behavior:

  • RuntimeException và con cháu → unchecked → compiler không ép khai throws hay catch. Tự do.
  • Các Exception khác (IOException, SQLException, ClassNotFoundException, InterruptedException) → checked → compiler bắt phải khai throws hoặc catch.

Chi tiết checked vs unchecked ở bài 4. Hiện tại nắm: hai nhóm có signature compiler khác nhau.

Exception phổ biến nhất

ExceptionKhi nào gặp
NullPointerExceptionDereference null: user.getName() khi user == null
IllegalArgumentExceptionArgument không hợp lệ: sqrt(-1)
IllegalStateExceptionState sai cho op: gọi method khi connection đã close
IndexOutOfBoundsExceptionArray/List index ngoài giới hạn
ArithmeticExceptionChia cho 0 (integer)
ClassCastExceptionCast sai kiểu
NumberFormatExceptionParse số sai: Integer.parseInt("abc")
IOExceptionDisk/network lỗi
InterruptedExceptionThread bị interrupt khi sleep/wait
💡 Quy tắc chọn exception khi throw
  • NPE: tham chiếu null không mong đợi. Objects.requireNonNull(x) throw NPE kèm message.
  • IllegalArgumentException: argument sai (giá trị, format). Validate input bằng cái này.
  • IllegalStateException: state object sai cho operation (method called out of order).
  • IndexOutOfBoundsException: index ngoài range.
  • UnsupportedOperationException: operation không hỗ trợ (add vào immutable list).

Chọn đúng exception class giúp reader + stack trace hiểu intent.

3. Cú pháp try/catch/finally

try {
    // code co the nem exception
} catch (SpecificException e) {
    // xu ly loai loi cu the
} finally {
    // cleanup - luon chay
}

Ví dụ cụ thể:

public static int parseOrDefault(String s, int defaultValue) {
    try {
        return Integer.parseInt(s);
    } catch (NumberFormatException e) {
        System.out.println("Cannot parse: " + s + " - fallback");
        return defaultValue;
    }
}

parseOrDefault("42", 0);      // 42
parseOrDefault("abc", 0);     // 0

Rule cú pháp:

  • try phải có ít nhất 1 catch, hoặc finally, hoặc cả hai.
  • try block không thể đứng một mình.
  • catch cụ thể trước, tổng quát sau.

4. Nhiều catch — thứ tự quan trọng

try {
    openFile("a.txt");
    parseContent();
} catch (FileNotFoundException e) {
    System.out.println("File not found");
} catch (IOException e) {
    System.out.println("I/O error: " + e.getMessage());
} catch (Exception e) {
    System.out.println("Unknown error");
}

Compiler check: catch cụ thể phải trước catch tổng quát, vì runtime match từ trên xuống, dừng ở match đầu tiên.

try { ... }
catch (Exception e) { ... }      // bat moi Exception
catch (IOException e) { ... }     // COMPILE ERROR - unreachable

Compiler error: "exception IOException has already been caught".

Multi-catch (Java 7+)

Xử lý nhiều exception giống nhau trong 1 block:

try {
    doSomething();
} catch (IOException | SQLException e) {
    log.error("Data access failed", e);
    throw new DataAccessException(e);
}

Rule:

  • Các exception type không có quan hệ kế thừa (không thể IOException | FileNotFoundException vì FNE là con IOException).
  • Biến ecommon supertype — ở đây kiểu thực là Exception. Chỉ gọi được method chung.
  • Biến exception e trong cú pháp multi-catch được compiler ngầm định là effectively final. Bạn tuyệt đối không được phép gán lại giá trị cho biến e bên trong khối catch (ví dụ: e = new IOException(...) sẽ gây lỗi biên dịch).

5. finally — luôn chạy

Connection conn = null;
try {
    conn = DriverManager.getConnection(url);
    doWork(conn);
} catch (SQLException e) {
    log.error("DB error", e);
    throw new RuntimeException(e);
} finally {
    if (conn != null) {
        try { conn.close(); }
        catch (SQLException e) { log.warn("Failed to close", e); }
    }
}
⚠️ Bẫy kinh điển: Exception Shadowing (Ngoại lệ bị che khuất)

conn.close() trong finally cũng ném được SQLException. Nếu doWork(conn) đã ném một ngoại lệ chính, finally vẫn phải chạy trước khi thoát method — và nếu close() ném tiếp thì ngoại lệ từ finally ghi đè hoàn toàn ngoại lệ gốc trong try.

Log production khi đó chỉ còn lỗi đóng kết nối, còn lỗi nghiệp vụ thật thì mất sạch. Đây chính là động lực ra đời của try-with-resources (bài 5).

finally chạy bất kể:

  • Try chạy xong bình thường → finally chạy.
  • Try ném exception, catch match → catch chạy → finally chạy.
  • Try ném exception, không có catch match → finally chạy → exception propagate.
  • Catch ném exception mới → finally chạy → exception mới propagate.

Chỉ 2 trường hợp finally KHÔNG chạy:

  1. System.exit(n) — JVM tắt luôn, không cleanup.
  2. JVM crash (kill -9, hardware fail).

Return trong finally — anti-pattern

public static int demo() {
    try {
        return 1;
    } finally {
        return 2;   // override return cua try
    }
}
// demo() tra 2

return trong finally override return của try/catch, và nuốt luôn exception đang bay (xem hình ở mục 7.1). IDE cảnh báo "finally block does not complete normally" — đừng return trong finally.

Try-with-resources thay finally cleanup

Nếu cleanup là close resource, try-with-resources (bài 5) gọn hơn finally thủ công. Ví dụ trên viết lại:

try (Connection conn = DriverManager.getConnection(url)) {
    doWork(conn);
} catch (SQLException e) {
    log.error("DB error", e);
    throw new RuntimeException(e);
}
// conn tu dong close, khong can finally

6. throw — chủ động ném

public static int divide(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException("Division by zero");
    }
    return a / b;
}

throw ném exception tại điểm đó. Execution dừng, JVM unwind stack tìm catch match. (Cơ chế unwind qua exception table chi tiết ở bài 3.)

Rule khi throw:

  • Dùng throw new <ExceptionClass>(message, cause).
  • Chọn exception class phù hợp ngữ nghĩa (xem bảng mục 2).
  • Luôn message có ý — ghi cả giá trị gây lỗi để debug:
    throw new IllegalArgumentException("age must be non-negative, got " + age);
    

7. Anti-patterns — AVOID

7.1 Nuốt exception — số 1

try {
    doWork();
} catch (Exception e) {
    // khong log, khong throw, khong lam gi
}

Anti-pattern số 1 của Java. Nhưng catch rỗng không phải cách duy nhất làm exception biến mất — return trong finally (mục 5) cũng vứt nó, và khó nhận ra hơn hẳn vì trong code không có khối catch nào để soi:

Hai cột cùng đổ về một kết cục: cột trái doWork ném IOException rơi vào catch rỗng nên luồng chạy tiếp với dữ liệu sai, cột phải finally return 2 nên method trả về coi như không lỗi, cả hai đều không để lại gì trong log

Fix cho cột trái:

  • Log: log.error("Failed to do work", e); (pass cả exception để có stack trace).
  • Rethrow: throw new RuntimeException(e); nếu không biết xử lý.
  • Domain wrap: throw new OrderProcessingException("Step X failed", e);.

7.2 Catch Exception hoặc Throwable tổng quát

try {
    doX();
} catch (Exception e) {   // Che cac bug code
    log.error("Error", e);
}

Catch Exception phủ lớn che NullPointerException, ClassCastException, etc. — thường là bug code cần fix, không phải handle.

Fix: catch chính xác exception bạn dự liệu (IOException, SQLException). Để RuntimeException propagate lên global handler.

7.3 printStackTrace() không log

try { ... } catch (Exception e) {
    e.printStackTrace();
}

In ra System.err. Production server System.err thường không đi vào log aggregator. Lỗi vào void.

Fix: logger framework (SLF4J, Logback):

log.error("Operation X failed", e);

7.4 Catch InterruptedException không restore flag

Khi một thread đang trong trạng thái block (như Thread.sleep(), Object.wait(), Thread.join()) và bị một luồng khác gọi interrupt(), JVM sẽ lập tức ném ra InterruptedException để đánh thức luồng bị chặn.

Vấn đề cực kỳ quan trọng: Ngay khi JVM ném ra InterruptedException, nó sẽ tự động xóa sạch (clear) cờ ngắt (interrupt flag) của thread đó về false.

Nếu bạn catch exception này mà không xử lý gì (nuốt ngoại lệ):

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Swallow exception and keep running
}

Hệ quả là các framework quản lý luồng cấp cao (như ExecutorService/Thread Pool) khi kiểm tra Thread.currentThread().isInterrupted() sẽ thấy cờ ngắt bằng false, từ đó không biết luồng này đang được yêu cầu dừng lại, dẫn đến rò rỉ luồng và treo ứng dụng.

Giải pháp chính xác (Khôi phục Interrupt Flag): Bạn phải luôn khôi phục lại trạng thái ngắt bằng cách gọi Thread.currentThread().interrupt() để báo hiệu cho các tầng quản lý phía trên:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Restore interrupt flag for current thread
    Thread.currentThread().interrupt();
    // Stop processing this task immediately
    return;
}

8. Pitfall tổng hợp

Nhầm 1: Nuốt exception. ✅ Log + rethrow hoặc domain wrap.

Nhầm 2: Catch Exception tổng quát che bug. ✅ Catch cụ thể; RuntimeException để propagate.

Nhầm 3: return trong finally. ✅ Không return trong finally. Dùng try-with-resources cleanup.

Nhầm 4: Thứ tự catch sai.

catch (Exception e) { ... }
catch (IOException e) { ... }   // UNREACHABLE

✅ Cụ thể trước, tổng quát sau.

Nhầm 5: printStackTrace() production. ✅ log.error(message, e);

9. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

Ghi chú: Effective Java Item 69 — exception không dùng cho control flow thông thường (if/else). Dùng exception cho tình huống thực sự "bất thường" — đảm bảo hiệu năng và code readability.

10. Tóm tắt

  • Cây Throwable: Throwable → Error / Exception; Exception → RuntimeException (unchecked) / checked.
  • Error không catch — JVM lỗi nặng.
  • Exception = lỗi ứng dụng. RuntimeException unchecked. Checked cho low-level IO/DB.
  • Chọn exception class đúng semantic: NPE, IllegalArgumentException, IllegalStateException, IndexOutOfBoundsException.
  • try/catch/finally: catch cụ thể trước, tổng quát sau. finally luôn chạy (trừ System.exit, JVM crash).
  • Không return trong finally — override try return.
  • Multi-catch (Java 7+): catch (A | B e) gộp xử lý.
  • Anti-pattern: nuốt exception, catch Exception tổng quát, printStackTrace production, InterruptedException không restore flag.

11. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn sau in gì và return bao nhiêu?
    public static int demo() {
      try {
          return 1;
      } finally {
          System.out.println("finally");
          return 2;
      }
    }
    System.out.println(demo());
  2. Q2
    Đoạn sau compile không?
    try { ... }
    catch (Exception e) { log.error(e); }
    catch (IOException e) { log.error(e); }
  3. Q3
    Vì sao nuốt exception là anti-pattern?
  4. Q4
    Cây Throwable có 3 tầng: Throwable → Error/Exception, Exception → RuntimeException/checked. Mỗi tầng catch khi nào?
  5. Q5
    Vì sao khi catch InterruptedException phải gọi Thread.currentThread().interrupt() trước khi xử lý tiếp?

Bài tiếp theo: Đọc stack trace và cause chain — giữ dấu vết lỗi qua mọi layer

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Đọc stack trace và cause chain — giữ dấu vết lỗi qua mọi layer