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.

Dưới đây là sơ đồ mô tả luồng tìm kiếm và cuộn ngược Call Stack của JVM:

graph TD
    A["main() method"] -->|goi| B["level1() method"]
    B -->|goi| C["level2() method"]
    C -->|goi| D["level3() method"]
    D -->|nem exception| E{"Co catch block?"}
    E -->|Khong| F["Pop level3 frame khoi stack"]
    F --> G{"level2() co catch block?"}
    G -->|Khong| H["Pop level2 frame khoi stack"]
    H --> I{"level1() co catch block?"}
    I -->|Co| J["Xu ly exception tai level1()"]
    I -->|Khong| K["Pop level1 frame khoi stack"]
    K --> L{"main() co catch block?"}
    L -->|Khong| M["JVM in stack trace & crash thread"]
    
    style D fill:#f9f,stroke:#333,stroke-width:2px
    style J fill:#bbf,stroke:#333,stroke-width:2px
    style M fill:#ff9999,stroke:#333,stroke-width:2px

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)

Hãy quan sát kỹ khối finally ở trên. Việc gọi conn.close() trong finally cũng có thể ném ra một SQLException.

Nếu phương thức doWork(conn) trong khối try gặp sự cố nặng và ném ra một ngoại lệ chính, JVM sẽ chuẩn bị đẩy ngoại lệ đó lên call stack. Tuy nhiên, trước khi thoát khỏi phương thức, khối finally buộc phải thực thi. Nếu tại đây conn.close() cũng thất bại và ném tiếp một SQLException khác, thì:

Ngoại lệ ném ra từ finally sẽ ghi đè (shadow) hoàn toàn và làm biến mất ngoại lệ gốc trong try.

Khi tra cứu log production, bạn chỉ thấy lỗi đóng kết nối (conn.close()) mà hoàn toàn mù tịt về lỗi nghiệp vụ thực tế xảy ra trước đó trong doWork. Đây là một điểm hạn chế rất lớn của Java phiên bản cũ, tạo động lực mạnh mẽ cho sự ra đời của cú pháp try-with-resources (chúng ta sẽ tìm hiểu kỹ ở 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. Hầu như luôn là bug — mất giá trị tính trong try, mất exception được throw.

IDE (IntelliJ, Eclipse) warning "finally block does not complete normally". Sửa: khô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. Lỗi xảy ra, program chạy tiếp như chưa có → bug ẩn, data corrupt, user complain. Debug không có thông tin.

Fix:

  • 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
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());

In finally rồi 2.

Flow:

  1. return 1 trong try — value 1 được chuẩn bị trả.
  2. Trước khi thực sự return, JVM chạy finally.
  3. System.out.println("finally") in.
  4. return 2 trong finally override return của try — method thật sự trả 2.

return trong finally là anti-pattern — nuốt return try và có thể nuốt exception chưa propagate. IDE warning. Rule: chỉ side-effect cleanup trong finally.

Q2
Đoạn sau compile không?
try { ... }
catch (Exception e) { log.error(e); }
catch (IOException e) { log.error(e); }

Không compile. catch (Exception e) bắt mọi Exception, bao gồm IOException. catch (IOException e) sau sẽ không bao giờ match → compiler báo "exception IOException has already been caught".

Fix: đảo thứ tự — catch cụ thể trước, tổng quát sau:

try { ... }
catch (IOException e) { log.error(e); }
catch (Exception e) { log.error(e); }

Compiler match theo thứ tự trên xuống, gặp match đầu tiên dừng.

Q3
Vì sao nuốt exception là anti-pattern?

Exception được ném nhưng không ai biết. Hệ quả:

  • Bug ẩn trong production: logic downstream chạy với data rỗng → bug ngẫu nhiên khó reproduce.
  • Debug cực khó: không có log, không có stack trace, developer lội code tìm không ra.
  • Mất observability: monitoring/metrics không biết có lỗi.

Fix:

  • Log: log.error("failed", e); — pass exception để có stack trace.
  • Rethrow: throw new RuntimeException(e);.
  • Wrap domain: throw new DataFetchException(e);.

Effective Java Item 77: "Don't ignore exceptions". Nếu thực sự muốn ignore (hiếm), comment rõ lý do.

Q4
Cây Throwable có 3 tầng: Throwable → Error/Exception, Exception → RuntimeException/checked. Mỗi tầng catch khi nào?

Throwable: root. Catch bắt mọi thứ, cả Error. Không nên — catch Error (OOM, StackOverflow) không giúp gì vì JVM đã bất ổn. Chỉ dùng cho global uncaught handler.

Error: JVM errors (OutOfMemoryError, StackOverflowError, NoClassDefFoundError). Không catch. Fail fast — restart process.

Exception: application errors. 2 nhóm:

  • RuntimeException (unchecked): thường là bug code (NPE, ClassCast). Không catch chỗ gọi — để propagate lên global handler, fix code gốc. Catch khi có fallback hợp lý (vd NumberFormatException parse user input → trả default).
  • Checked Exception (IOException, SQLException): compiler ép khai throws hoặc catch. Catch nơi biết cách xử lý; nếu không, throw lên hoặc wrap thành RuntimeException.

Rule: catch càng cụ thể càng tốt.

Q5
Vì sao khi catch InterruptedException phải gọi Thread.currentThread().interrupt() trước khi xử lý tiếp?

Ngay khi ném InterruptedException, JVM tự động xoá cờ ngắt (interrupt flag) của thread về false. Nếu bạn nuốt exception hoặc chỉ wrap rồi rethrow mà không khôi phục cờ, tín hiệu "thread này được yêu cầu dừng" biến mất.

Hệ quả: các tầng quản lý phía trên — ExecutorService, vòng lặp worker kiểm tra isInterrupted() — không còn biết có yêu cầu dừng, nên không thể kết thúc task sớm hoặc shutdown gọn gàng như thiết kế.

Gọi Thread.currentThread().interrupt() bật lại cờ ngắt, bảo toàn tín hiệu cho tầng trên, rồi bạn mới return hoặc rethrow. Đây là quy tắc bắt buộc mỗi khi catch InterruptedException mà không tự xử lý trọn vẹn.

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 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