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/finallysemantic.throwchủ độ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:
- Tự xử: có bình cứu hoả → dùng → tiếp tục làm việc. Tương đương
catch+ continue. - 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ường | Java |
|---|---|
| Sự cố phát sinh | throw new Exception(...) |
| Tự xử | catch (Exception e) { ... } |
| Báo lên tầng trên | Khô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:

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.
OutOfMemoryErrorvề 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:
RuntimeExceptionvà con cháu → unchecked → compiler không ép khaithrowshay catch. Tự do.- Các
Exceptionkhác (IOException, SQLException, ClassNotFoundException, InterruptedException) → checked → compiler bắt phải khaithrowshoặ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
| Exception | Khi nào gặp |
|---|---|
NullPointerException | Dereference null: user.getName() khi user == null |
IllegalArgumentException | Argument không hợp lệ: sqrt(-1) |
IllegalStateException | State sai cho op: gọi method khi connection đã close |
IndexOutOfBoundsException | Array/List index ngoài giới hạn |
ArithmeticException | Chia cho 0 (integer) |
ClassCastException | Cast sai kiểu |
NumberFormatException | Parse số sai: Integer.parseInt("abc") |
IOException | Disk/network lỗi |
InterruptedException | Thread bị interrupt khi sleep/wait |
- 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:
tryphải có ít nhất 1catch, hoặcfinally, hoặc cả hai.tryblock không thể đứng một mình.catchcụ 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 | FileNotFoundExceptionvì FNE là con IOException). - Biến
elà common supertype — ở đây kiểu thực làException. Chỉ gọi được method chung. - Biến exception
etrong 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ếnebên trong khốicatch(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); }
}
}
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:
System.exit(n)— JVM tắt luôn, không cleanup.- 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:

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
Spec / reference chính thức:
- JLS §14.20 — The try statement — cú pháp try/catch/finally.
- JLS §11 — Exceptions — lý thuyết, kinds.
- Throwable javadoc — API getMessage, getCause, getStackTrace.
- Effective Java Item 69 "Use exceptions only for exceptional conditions"; Item 77 "Don't ignore exceptions".
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. Errorkhông catch — JVM lỗi nặng.Exception= lỗi ứng dụng.RuntimeExceptionunchecked. 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.finallyluô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
- 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()); - Q2Đoạn sau compile không?
try { ... } catch (Exception e) { log.error(e); } catch (IOException e) { log.error(e); } - Q3Vì sao nuốt exception là anti-pattern?
- Q4Cây Throwable có 3 tầng: Throwable → Error/Exception, Exception → RuntimeException/checked. Mỗi tầng catch khi nào?
- Q5Vì sao khi catch
InterruptedExceptionphải gọiThread.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
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