Module 4 bạn học method + call stack. Khi method lâm vào tình huống không tiếp tục bình thường được — file không mở được, chia cho 0, kết nối DB rớt — nó có hai lựa chọn: trả giá trị đặc biệt (null, -1), hoặc ném exception. Exception là cơ chế Java để báo lỗi và unwind (quay ngược) call stack đến một điểm bắt được.
Bài này giải thích try/catch/finally, thứ tự bắt nhiều exception, rule finally chạy khi nào, multi-catch Java 7+, và hai anti-pattern phổ biến nhất: nuốt exception và bắt Exception chung chung.
1. Analogy — giao hàng gặp sự cố
Shipper đi giao đơn. Đi đường, hỏng xe — gặp exception. Họ hai lựa chọn: (1) tự xử (gọi thợ sửa) — catch + xử lý, (2) báo cấp trên quay lại lấy đơn — rethrow / propagate. Dù chọn gì, họ luôn làm 1 việc cuối: đóng sổ theo dõi (báo end-of-day cho công ty) — đó là finally.
| Đời thường | Java |
|---|---|
| Sự cố trên đường | throw new Exception(...) |
| Shipper tự xử | catch (Exception e) { ... } |
| Báo lại cấp trên | Không catch, để propagate lên caller |
| Đóng sổ theo dõi (luôn làm) | finally { ... } |
💡 💡 Cách nhớ
try = "thử khối này, có thể lỗi". catch = "nếu lỗi, xử thế này". finally = "dù lỗi hay không, luôn làm đoạn cuối này".
2. Cú pháp cơ bản
try {
// code co the nem exception
} catch (SpecificException e) {
// xu ly loai loi cu the
} finally {
// cleanup — luon chay
}
Ví dụ:
public static int parseOrDefault(String s, int defaultValue) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
System.out.println("Cannot parse: " + s + " — fallback to " + defaultValue);
return defaultValue;
}
}
parseOrDefault("42", 0); // 42
parseOrDefault("abc", 0); // 0, in log "Cannot parse: abc — fallback to 0"
Rule:
- Có
tryrồi phải có ít nhất 1catch, hoặcfinally, hoặc cả hai. - Block
trykhông thể đứng một mình. - Catch cụ thể trước, tổng quát sau (nếu có nhiều catch).
3. 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 kiểm tra exception là subtype của nhau — catch cụ thể phải đứng trước catch tổng quát, vì runtime match theo thứ tự trên xuống.
try { ... }
catch (Exception e) { ... } // bat moi exception
catch (IOException e) { ... } // COMPILE ERROR — IOException da bi catch(Exception) bat het
Java bắt lỗi compile-time: thứ tự sai, catch sau không bao giờ match → unreachable code.
3.1 Multi-catch (Java 7+)
Khi nhiều loại exception xử lý giống nhau, gộp trong 1 catch với |:
try {
doSomething();
} catch (IOException | SQLException e) {
log.error("Data access failed", e);
throw new DataAccessException(e);
}
Rule multi-catch:
- Các exception type không có quan hệ kế thừa với nhau. Viết
IOException | FileNotFoundExceptioncompile error (FileNotFoundException là con của IOException → lặp). - Biến
ecó kiểu common supertype (ở đây làExceptionhoặc kiểu chung gần nhất).
4. 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, khong throw them */ }
}
}
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 lên caller.
- Catch ném exception → finally chạy → exception propagate.
Chỉ 2 trường hợp finally KHÔNG chạy:
System.exit()— JVM tắt luôn.- JVM crash (OOM, hardware, v.v.).
4.1 Return trong finally — cẩn thận
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. Nhiều code style ban hoàn toàn.
⚠️ ⚠️ Đừng return từ finally
IDE warning "finally block does not complete normally". Dùng try-with-resources (bài 3) thay vì finally thủ công khi có thể — an toàn hơn, ít bug hơn.
5. throw — chủ động ném exception
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 cho đến khi tìm được catch match.
Rule:
- Dùng
throw new <ExceptionClass>(message, cause). - Chọn exception class phù hợp ngữ nghĩa:
IllegalArgumentExceptioncho "input sai",IllegalStateExceptioncho "state sai",NullPointerExceptioncho "null không mong đợi". - Luôn message có ý — ghi cả giá trị gây lỗi để debug:
throw new IllegalArgumentException("age must be non-negative, got " + age);
6. Exception hierarchy
Throwable
├── Error (JVM lỗi nặng — không catch)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception
├── RuntimeException (unchecked — xem bài 2)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IndexOutOfBoundsException
│ └── ...
└── (checked exception) (bị buộc khai throws hoặc catch)
├── IOException
├── SQLException
└── ...
Throwable— gốc của tất cả. Bắt được bằngcatch (Throwable t), nhưng thường không nên.Error— lỗi nặng JVM. Không catch (OOM, StackOverflow). Nếu gặp thì app thường đã ở trạng thái không ổn định.Exception— lỗi "ứng dụng". Chia thành checked và unchecked — bài 2 đi sâu.
7. Anti-patterns — AVOID
7.1 Nuốt exception
try {
doWork();
} catch (Exception e) {
// khong log, khong throw, khong lam gi
}
Đây là anti-pattern số 1 trong Java. Lỗi xảy ra, chương trình chạy tiếp như chưa có gì — bug ẩn trong production, không ai thấy cho đến khi khách hàng than phiền.
Fix:
- Log exception đầy đủ (kèm stack trace):
log.error("Failed to do work", e); - Hoặc throw lại (rethrow) nếu không biết xử lý:
throw new RuntimeException(e); - Hoặc throw exception nghiệp vụ:
throw new OrderProcessingException(e);
7.2 Catch Exception / Throwable tổng quát
try {
doX();
} catch (Exception e) {
// bat moi thu — khong phan biet loai nao
}
Catch Exception hoặc Throwable "phủ lớn" thường che giấu bug: NullPointerException do code sai, ClassCastException do design vấn đề — tất cả chạy qua block này và có thể ghi log "nhẹ nhàng", không fix gốc.
Fix: catch chính xác kiểu bạn dự liệu (IOException, SQLException). Để RuntimeException tự propagate — nó thường là bug code, đừng che.
7.3 catch rồi printStackTrace() mà không làm gì khác
try { ... } catch (Exception e) {
e.printStackTrace();
}
e.printStackTrace() in ra System.err — trong server production, System.err thường không đi vào log aggregator (Elastic, Datadog). Lỗi vào void.
Fix: dùng logger framework (SLF4J / Logback / Log4j):
log.error("Operation X failed", e);
8. Custom exception — khi nào viết?
public class OrderNotFoundException extends RuntimeException {
private final long orderId;
public OrderNotFoundException(long orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
public long getOrderId() { return orderId; }
}
Tạo exception riêng khi:
- Bạn muốn caller catch theo semantics nghiệp vụ:
catch (OrderNotFoundException e)rõ hơncatch (IllegalArgumentException e). - Cần mang data context ngoài message:
orderId,userId— dễ log/metric.
Đặt tên: đuôi Exception (OrderNotFoundException, InsufficientBalanceException). Extends RuntimeException cho unchecked (mặc định trong domain code modern), extends Exception chỉ khi chắc chắn cần checked (bài 2).
9. Pitfall tổng hợp
❌ Nhầm 1: Nuốt exception — catch(...) {} rỗng.
✅ Log + rethrow, không bao giờ nuốt.
❌ Nhầm 2: Catch Exception tổng quát che bug.
✅ Catch kiểu cụ thể; RuntimeException để propagate.
❌ Nhầm 3: return trong finally.
✅ Tránh; dùng try-with-resources cho cleanup.
❌ Nhầm 4: Thứ tự catch sai (cha trước con).
catch (Exception e) { ... }
catch (IOException e) { ... } // UNREACHABLE
✅ Đảo: cụ thể trước, tổng quát sau.
❌ Nhầm 5: throw e; rồi thêm thao tác (không bao giờ chạy).
throw e;
log.info("after throw"); // UNREACHABLE
✅ Đặt throw cuối block catch.
10. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec / reference chính thức:
- JLS §14.20 — The try statement — cú pháp try/catch/finally/try-with-resources.
- JLS §11 — Exceptions — lý thuyết exception, kinds, checked vs unchecked.
- JVMS §2.10 — Exceptions — bytecode level. Mỗi method có exception table ghi range bytecode + handler — JVM tra khi throw.
- Throwable javadoc — method
getMessage,getCause,getStackTrace,addSuppressed. - Effective Java Item 77: "Don't ignore exceptions".
Ghi chú: JVMS §2.10 mô tả exception table — thay vì "if check" tốn, exception dispatch là lookup table thuần tuý ở cấp bytecode. Đây là lý do throw + catch trong Java rẻ — miễn là stack trace không phải fill hot path.
11. Tóm tắt
try/catch/finally= cơ chế báo/bắt lỗi mà không sập chương trình.trybắt buộc cócatchhoặcfinally(hoặc cả hai).- Catch cụ thể trước, tổng quát sau — compile error nếu đảo.
- Multi-catch
catch (A | B e)(Java 7+) — gộp xử lý. finallyluôn chạy, trừSystem.exit()hoặc JVM crash. Khôngreturntrong finally.throw new X(...)chủ động ném. Chọn exception class phù hợp ngữ nghĩa.- Hierarchy:
Throwable→Error/Exception.Exception→RuntimeException(unchecked) hoặc checked. - Anti-pattern: nuốt exception, catch
Exceptiontổng quát,printStackTracekhông log. - Custom exception cho nghiệp vụ — extends
RuntimeExceptionmặc định cho domain code.
12. 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());
▸
public static int demo() {
try {
return 1;
} finally {
System.out.println("finally");
return 2;
}
}
System.out.println(demo());In finally rồi 2.
Flow:
return 1trong try — giá trị1được chuẩn bị trả.- Trước khi return, JVM chạy
finally. System.out.println("finally")in.return 2trong finally override return của try — method thật sự trả2.
return trong finally là anti-pattern: nó nuốt return của try/catch và có thể nuốt cả exception chưa propagate. IDE luôn warning. Rule: chỉ có side-effect (cleanup) trong finally, không return.
Q2Đoạn sau compile không?try { ... }
catch (Exception e) { log.error(e); }
catch (IOException e) { log.error(e); }
▸
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 cả IOException. catch (IOException e) đứng sau sẽ không bao giờ match — compiler báo error "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); }Quy tắc này áp dụng cho mọi chuỗi catch — compiler chạy match theo thứ tự trên xuống, gặp match đầu tiên dừng.
Q3Vì sao "nuốt exception" là anti-pattern?try {
fetchFromApi();
} catch (IOException e) {
// khong log, khong throw
}
▸
try {
fetchFromApi();
} catch (IOException e) {
// khong log, khong throw
}Exception được ném nhưng không ai biết nó xảy ra. Hệ quả:
- Bug ẩn trong production: API fail, fetch không có kết quả, 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 nguyên nhân.
- Mất observability: monitoring/metrics không biết có lỗi.
Fix (chọn 1 tuỳ ngữ cảnh):
- Log + continue:
log.error("fetchFromApi failed", e);rồi xử lý fallback. - Rethrow:
throw new RuntimeException(e);nếu không có fallback hợp lý. - Wrap thành domain exception:
throw new DataFetchException(e);.
Effective Java Item 77 nói thẳng: "Don't ignore exceptions". Nếu thực sự muốn ignore (hiếm), comment rõ lý do.
Q4Đoạn sau in gì?try {
System.out.println("try");
throw new RuntimeException("oops");
} catch (RuntimeException e) {
System.out.println("catch: " + e.getMessage());
throw new IllegalStateException("rethrow", e);
} finally {
System.out.println("finally");
}
▸
try {
System.out.println("try");
throw new RuntimeException("oops");
} catch (RuntimeException e) {
System.out.println("catch: " + e.getMessage());
throw new IllegalStateException("rethrow", e);
} finally {
System.out.println("finally");
}In:
try
catch: oops
finallySau đó ném IllegalStateException: rethrow (cause là RuntimeException: oops).
Flow:
- Try in "try", throw
RuntimeException. - Catch match → in "catch: oops" → throw
IllegalStateException. - Trước khi propagate, finally chạy → in "finally".
- Sau finally,
IllegalStateExceptionpropagate lên caller. Stack trace giữ cả original exception quagetCause().
Pattern "wrap and rethrow với cause" cực phổ biến — giữ thông tin gốc trong khi dùng exception type phù hợp cho caller.
Q5Khi nào tạo custom exception (ví dụ OrderNotFoundException)?▸
OrderNotFoundException)?Tạo custom exception khi bạn cần một (hoặc nhiều) trong các mục tiêu:
- Catch theo semantics nghiệp vụ:
catch (OrderNotFoundException e)biểu đạt ý định rõ hơncatch (IllegalStateException e). Reader hiểu ngay "đây xử lý case order không tồn tại". - Mang context data: custom exception có field (
orderId,userId) — dễ log có cấu trúc, dễ metric theo dimension. - Hierarchy nghiệp vụ:
abstract class OrderException extends RuntimeExceptionvới subclassOrderNotFoundException,OrderAlreadyPaidException,OrderExpiredException. Caller chọn granularity:catch (OrderException e)bắt hết, hoặc catch từng loại. - API layer translation: exception từ infrastructure (SQLException) wrap thành domain exception ở repository layer.
Mặc định: extends RuntimeException — unchecked, không ép caller try/catch. Extends Exception chỉ khi thật sự muốn buộc caller xử lý (bài 2 giải thích rõ).
Bài tiếp theo: Checked vs Unchecked — cuộc tranh cãi kéo dài 20 năm