Menu

try-catch trong Java: xử lý ngoại lệ mà không bị sập

Cách dùng try-catch trong Java để xử lý ngoại lệ: bắt đúng loại cụ thể, khối finally, try-with-resources và những sai lầm khiến bug bị che giấu.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Bắt ngoại lệ thay vì để chương trình sập

Bạn đã biết rằng khi có gì đó sai, Java sẽ ném ra một ngoại lệ (exception), và một ngoại lệ không được xử lý sẽ dừng chương trình kèm một stack trace. Khối try-catch là cách bạn giành lại quyền kiểm soát: bạn bọc đoạn code rủi ro trong try, và nếu nó ném ngoại lệ, Java nhảy tới khối catch tương ứng thay vì làm chương trình sập.

Ngay khi 10 / 0 ném ngoại lệ, phần còn lại của khối try bị bỏ qua và luồng điều khiển chuyển sang catch. Sau khi catch chạy xong, chương trình tiếp tục bình thường, không bị dừng.

Biến trong catch giữ ngoại lệ

e trong catch (ArithmeticException e) là một đối tượng thật. Nó mang thông tin về điều đã xảy ra, hữu ích nhất là một thông báo:

e.getMessage() trả về một mô tả ngắn. Khi gỡ lỗi, e.printStackTrace() in ra toàn bộ stack trace cho thấy chính xác ngoại lệ đến từ đâu: hãy dùng nó khi chỉ thông báo thôi chưa đủ để tìm ra nguyên nhân.

Bắt đúng loại cụ thể, không bắt tất cả

Một khối catch chỉ bắt các ngoại lệ khớp với kiểu đã khai báo (hoặc lớp con của nó). Sai lầm lớn nhất của người mới là bắt Exception để vấn đề "biến mất":

// Don't do this - it hides real bugs
try {
    doWork();
} catch (Exception e) {
    // swallows NullPointerException, typos, logic errors... everything
}

Hãy bắt kiểu hẹp nhất mà bạn thật sự xử lý được. Nếu bạn dự kiến đầu vào số không hợp lệ, hãy bắt NumberFormatException. Mọi thứ bạn không lường trước nên được để lan ra ngoài, để bạn thật sự biết đến nó thay vì lặng lẽ chạy tiếp trong một trạng thái hỏng.

Xử lý nhiều loại ngoại lệ

Bạn có thể xếp chồng nhiều khối catch. Java kiểm tra chúng từ trên xuống dưới và chạy khối đầu tiên khớp, nên hãy sắp xếp từ cụ thể nhất đến chung nhất:

Khi hai loại có cùng cách xử lý, hãy dùng một multi-catch với | thay vì lặp lại khối lệnh:

Một điểm cần lưu ý: kiểu chung hơn phải đứng sau các kiểu cụ thể. Đặt catch (Exception e) lên đầu khiến các khối cụ thể hơn phía sau không bao giờ được chạm tới, và trình biên dịch sẽ từ chối.

finally luôn chạy

Khối finally chạy sau try và mọi catch, bất kể chuyện gì đã xảy ra: thành công, một ngoại lệ đã bị bắt, hay thậm chí return sớm. Đây là nơi dành cho việc dọn dẹp luôn phải diễn ra.

"Closing resource" được in ra dù ngoại lệ có xảy ra hay không. Tuy vậy, hãy tránh return từ bên trong finally: nó có thể lặng lẽ bỏ qua một ngoại lệ hoặc ghi đè giá trị được trả về từ khối try.

try-with-resources tự đóng giúp bạn

Khi làm việc với thứ phải được đóng lại, như một tệp, một kết nối mạng hay một câu lệnh cơ sở dữ liệu, khai báo nó bên trong try (...) sẽ tự động đóng nó khi khối lệnh kết thúc, kể cả khi có ngoại lệ được ném ra. Mọi kiểu triển khai interface AutoCloseable đều dùng được.

Cách này thay thế mẫu cũ là mở trong try rồi đóng trong finally, và ít lỗi hơn vì bạn không thể quên đóng. Hãy ưu tiên nó mỗi khi làm việc với một tài nguyên cần đóng.

Đừng dùng ngoại lệ cho luồng điều khiển thông thường

try-catch dành cho tình huống ngoại lệ, không phải điều kiện bình thường. Bắt một ngoại lệ tốn kém hơn một phép kiểm tra đơn giản, và khiến code khó đọc hơn. Nếu bạn có thể kiểm tra điều kiện trước bằng câu lệnh if-else, hãy làm như vậy:

// Avoid: using a catch to test for a missing key
try {
    process(map.get(key).trim());
} catch (NullPointerException e) {
    // handle missing key
}

// Prefer: check explicitly
String value = map.get(key);
if (value != null) {
    process(value.trim());
}

Hãy để dành catch cho những thứ thật sự nằm ngoài tầm kiểm soát của bạn: đầu vào sai từ người dùng, tệp bị thiếu, lỗi mạng.

Tiếp theo: NullPointerException

Ngoại lệ phổ biến nhất mà bạn sẽ bắt (và gây ra) trong Java là NullPointerException: nó xuất hiện ngay khi bạn gọi một phương thức trên thứ hóa ra là null. Tiếp theo chúng ta sẽ đào sâu xem chính xác điều gì gây ra nó, cách đọc stack trace của nó và những thói quen giúp nó không xảy ra ngay từ đầu.

Câu hỏi thường gặp

Dùng try-catch trong Java như thế nào?

Đặt đoạn code có thể ném ngoại lệ vào khối try, rồi thêm khối catch ghi rõ loại ngoại lệ bạn muốn xử lý: try { risky(); } catch (IOException e) { ... }. Nếu code trong try ném ra ngoại lệ khớp, Java nhảy thẳng tới khối catch thay vì làm chương trình sập. Biến (e) giữ đối tượng ngoại lệ để bạn đọc thông báo bằng e.getMessage().

Khối finally trong Java dùng để làm gì?

Khối finally chạy sau try/catch trong mọi trường hợp: dù code thành công, ném ngoại lệ hay thậm chí return sớm. Đây là nơi dành cho việc dọn dẹp luôn phải xảy ra, như đóng tệp hay giải phóng khóa. Để đóng tài nguyên, try-with-resources thường gọn hơn vì nó tự động đóng chúng.

Nên bắt Exception hay một loại ngoại lệ cụ thể?

Hãy bắt loại cụ thể nhất mà bạn thực sự xử lý được. Bắt Exception (hoặc tệ hơn, Throwable) sẽ nuốt mọi vấn đề, kể cả các bug như NullPointerException, và che giấu nguyên nhân thật. Hãy bắt NumberFormatException nếu đó là điều bạn dự kiến, và để các ngoại lệ bất ngờ lan ra ngoài để bạn biết đến chúng.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu