Java — Từ Zero đến Senior/try / catch / finally — cây Throwable và cơ chế throw
~20 phútException HandlingMiễn phí

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 (stack trace build, exception table bytecode, cause chain, suppressed) đi vào bài kế tiếp — để 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 { ... }

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

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); }
    }
}

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 4) 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 chi tiết ở bài 2.)

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

try { Thread.sleep(1000); }
catch (InterruptedException e) {
    // khong lam gi
}

InterruptedException khi throw clear interrupt flag. Code caller check Thread.isInterrupted() không thấy tín hiệu → thread không dừng. Bài 10.1 đã chi tiết.

Fix: restore:

catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    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.

Bài tiếp theo: Stack trace và cause chain — cơ chế bên dưới exception

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