Java — Từ Zero đến Senior/Exception Handling/try / catch / finally — báo lỗi mà không sập chương trình
1/5
~18 phútException Handling

try / catch / finally — báo lỗi mà không sập chương trình

Cơ chế throw và catch exception, thứ tự bắt theo kiểu, finally chạy khi nào, multi-catch Java 7+, và vì sao 'nuốt exception' là anti-pattern kinh điển nhất.

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ườngJava
Sự cố trên đườngthrow new Exception(...)
Shipper tự xửcatch (Exception e) { ... }
Báo lại cấp trênKhô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:

  • try rồi phải có ít nhất 1 catch, hoặc finally, hoặc cả hai.
  • Block try khô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 | FileNotFoundException compile error (FileNotFoundException là con của IOException → lặp).
  • Biến e có kiểu common supertype (ở đây là Exception hoặ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:

  1. System.exit() — JVM tắt luôn.
  2. 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: IllegalArgumentException cho "input sai", IllegalStateException cho "state sai", NullPointerException cho "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ằng catch (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ơn catch (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:

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.
  • try bắt buộc có catch hoặc finally (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ý.
  • finally luôn chạy, trừ System.exit() hoặc JVM crash. Không return trong finally.
  • throw new X(...) chủ động ném. Chọn exception class phù hợp ngữ nghĩa.
  • Hierarchy: ThrowableError / Exception. ExceptionRuntimeException (unchecked) hoặc checked.
  • Anti-pattern: nuốt exception, catch Exception tổng quát, printStackTrace không log.
  • Custom exception cho nghiệp vụ — extends RuntimeException mặc định cho domain code.

12. 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 — giá trị 1 được chuẩn bị trả.
  2. Trước khi 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: 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); }

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.

Q3
Vì sao "nuốt exception" là anti-pattern?
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");
}

In:

try
catch: oops
finally

Sau đó ném IllegalStateException: rethrow (cause là RuntimeException: oops).

Flow:

  1. Try in "try", throw RuntimeException.
  2. Catch match → in "catch: oops" → throw IllegalStateException.
  3. Trước khi propagate, finally chạy → in "finally".
  4. Sau finally, IllegalStateException propagate lên caller. Stack trace giữ cả original exception qua getCause().

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.

Q5
Khi nào tạo custom exception (ví dụ 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ơn catch (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 RuntimeException với subclass OrderNotFoundException, 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