Menu

try catch trong PHP: bắt ngoại lệ, finally và Throwable

Trong PHP, code có thể lỗi được đặt trong khối try { }, và catch (Exception $e) { } chạy khi code đó ném ra ngoại lệ, với lý do nằm trong $e->getMessage(). Tìm hiểu cách bắt nhiều loại ngoại lệ, finally, vì sao catch (Exception) bỏ sót TypeError và DivisionByZeroError (hãy bắt Throwable), ném lại ngoại lệ và trình xử lý toàn cục.

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.

Trong PHP, bạn đặt code có thể lỗi bên trong try { ... } và xử lý lỗi trong catch (Exception $e) { ... }. Nếu có gì đó trong khối try ném ra ngoại lệ (exception), PHP nhảy thẳng tới khối catch phù hợp, và $e->getMessage() cho bạn biết lỗi gì đã xảy ra. Nếu không có gì bị ném, khối catch sẽ bị bỏ qua.

Đổi divide(10, 0) thành divide(10, 5) rồi chạy lại: không có ngoại lệ, nên not printed được in ra và khối catch bị bỏ qua. Không có try, ngoại lệ sẽ kết thúc script với lỗi nghiêm trọng "Uncaught". Cách tự ném ngoại lệ và viết lớp ngoại lệ riêng có ở trang ngoại lệ.

Cú pháp try catch

Dạng đầy đủ có ba phần. Chỉ try là bắt buộc, kèm theo ít nhất một catch hoặc một finally:

try {
    // code that may throw
} catch (SomeException $e) {
    // runs if SomeException (or a subclass) was thrown
} catch (OtherException | ThirdException $e) {
    // runs for either of these types
} finally {
    // always runs, thrown or not
}

Kiểu trong catch quyết định nó bắt những gì: chính lớp đó và mọi lớp kế thừa từ nó. Từ PHP 8.0, biến là tùy chọn khi bạn không cần đến: catch (JsonException) { ... }.

Bắt Exception, Error hay Throwable

Từ PHP 7, các lỗi mà chính PHP phát hiện (gọi một phương thức không tồn tại, sai kiểu đối số, chia cho 0) ném ra một Error, không phải Exception, và PHP 8 đã biến nhiều cảnh báo cũ thành TypeError và ValueError. Cả hai đều cài đặt interface Throwable, nhưng không cái nào kế thừa cái nào, nên catch (Exception $e) không bắt được Error:

Throwable
├── Exception                 (problems a program expects and handles)
│   ├── InvalidArgumentException, RuntimeException, JsonException, ...
└── Error                     (engine errors, usually a bug in the code)
    ├── TypeError
    ├── ValueError
    ├── ArithmeticError
    │   └── DivisionByZeroError
    └── UnhandledMatchError, ...

Chạy đoạn này để xem khối catch nào xử lý từng vấn đề:

Vậy nên bắt loại nào?

  • catch (SomeSpecificException $e) khi bạn biết điều gì có thể sai và cần làm gì với nó. Phần lớn code thuộc trường hợp này.
  • catch (Exception $e) cho "mọi thứ mà một thư viện hoặc code của tôi cố ý ném ra".
  • catch (Throwable $e) ở tầng ngoài cùng (trình xử lý request, trình chạy job) để ghi log mọi lỗi, kể cả lỗi của chính PHP, và hiển thị một thông báo thân thiện.

Bắt nhiều loại ngoại lệ

Viết nhiều khối catch để xử lý các kiểu khác nhau theo những cách khác nhau. PHP thử chúng từ trên xuống dưới và dùng khối khớp đầu tiên, nên kiểu cụ thể phải đứng trước kiểu cha của nó, nếu không kiểu cha sẽ bắt hết mọi thứ trước. Khi nhiều kiểu được xử lý giống nhau, hãy gộp chúng vào một catch bằng |:

Cả ba lớp đều kế thừa từ Exception, nên một khối catch (Exception $e) duy nhất cũng bắt được chúng, nhưng khi đó code không thể trả lời 404 cho một lỗi và 400 cho các lỗi còn lại.

finally: code luôn được chạy

Khối finally chạy sau try và catch bất kể chúng kết thúc thế nào: bình thường, với một ngoại lệ đã được bắt, với một ngoại lệ không ai bắt, hay với một return. Đây là nơi giải phóng những gì khối try đã chiếm giữ: đóng tệp, nhả khóa, dừng bộ đếm giờ.

Với good.txt, tệp được đóng trước khi saved được in ra, vì finally chạy trước khi hàm thật sự trả về. Với bad.txt, bên trong process() không có catch: finally đóng tệp, rồi ngoại lệ tiếp tục đi lên tới khối catch của nơi gọi hàm.

Tránh return bên trong finally: nó thay thế giá trị mà khối try đã trả về và lặng lẽ bỏ đi một ngoại lệ đang trên đường thoát ra ngoài:

function f(): string
{
    try {
        return 'from try';
    } finally {
        return 'from finally';
    }
}

function g(): string
{
    try {
        throw new RuntimeException('lost');
    } finally {
        return 'no exception reaches the caller';
    }
}

echo f(); // from finally
echo g(); // no exception reaches the caller

Ném lại hoặc bọc một ngoại lệ

Đôi khi khối catch chỉ xử lý được một phần vấn đề: ghi log, dọn dẹp, rồi để nó đi tiếp. throw $e; ném lại chính ngoại lệ đó. Thường gặp hơn là bạn muốn thêm ngữ cảnh mà vẫn giữ nguyên nguyên nhân gốc, và đó là việc của đối số thứ ba trong hàm khởi tạo, $previous:

Nơi gọi hàm chỉ phải xử lý một ConfigException và không cần biết cấu hình là JSON, trong khi getPrevious() giữ lại chi tiết cấp thấp để ghi log.

Ngoại lệ không được bắt và trình xử lý toàn cục

Một ngoại lệ không được catch nào xử lý sẽ kết thúc script với một lỗi nghiêm trọng, kèm theo tên lớp, thông báo, tệp và dòng, cùng stack trace:

<?php
// No try/catch anywhere in this file
function charge(int $cents): void
{
    if ($cents <= 0) {
        throw new DomainException("Amount must be positive, got $cents");
    }
}

charge(500);
echo "first charge ok\n";
charge(-1);
echo "never printed\n";

PHP in ra first charge ok rồi đến:

PHP Fatal error:  Uncaught DomainException: Amount must be positive, got -1 in /home/index.php:6
Stack trace:
#0 /home/index.php(12): charge()
#1 {main}
  thrown in /home/index.php on line 6

Trên một trang web đang chạy thật, thông báo đó không bao giờ được hiện ra cho khách truy cập. set_exception_handler() đăng ký một hàm nhận mọi ngoại lệ không ai bắt, một chỗ duy nhất để ghi log và in ra một trang lịch sự:

Trình xử lý chạy rồi script dừng lại; không có cách nào để chạy tiếp sau throw. Việc thông báo lỗi của chính PHP có hiện trên trang hay không do display_errors quyết định, được trình bày ở trang báo lỗi.

Cảnh báo không phải là ngoại lệ

try/catch chỉ thấy những gì được ném ra. Nhiều hàm PHP cũ không ném ngoại lệ: chúng trả về false hoặc null và, nhiều nhất, in ra một cảnh báo. json_decode() là trường hợp điển hình, và bọc nó trong try chẳng thay đổi gì cho tới khi bạn yêu cầu nó ném ngoại lệ:

Với các hàm không có cờ như vậy, hãy kiểm tra giá trị trả về (if ($handle === false)), hoặc kiểm tra điều kiện trước (file_exists(), isset()). Một set_error_handler() ném ra ErrorException sẽ biến mọi cảnh báo thành ngoại lệ thật nếu bạn muốn một quy tắc cho tất cả.

Công thức: kiểm tra dữ liệu form bằng ngoại lệ

Một mẫu thực tế: các hàm kiểm tra ném ra ngoại lệ, trang web bắt lấy và hiển thị thông báo cạnh form. Chạy khối này, rồi gõ một số tuổi vào form và bấm Send: cùng script đó sẽ chạy lại với $_POST đã có dữ liệu.

Hãy thử để trống ô, rồi nhập twelve, 9 và 30. Mỗi quy tắc không đạt sẽ ném ra ngoại lệ với thông báo riêng, và một khối catch biến tất cả thành cùng một dòng chữ đỏ. Trang form phát triển tiếp ý này với nhiều trường và lỗi hiển thị cạnh từng trường.

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

try catch trong PHP hoạt động thế nào?

PHP chạy code bên trong try { }. Nếu có gì đó trong khối ném ra ngoại lệ, PHP bỏ qua phần còn lại của khối try và chạy khối catch đầu tiên có kiểu khớp, với đối tượng được ném nằm trong biến: catch (Exception $e) { echo $e->getMessage(); }. Nếu không có gì bị ném, các khối catch sẽ bị bỏ qua.

Làm sao để bắt mọi ngoại lệ và lỗi trong PHP?

Bắt Throwable: catch (Throwable $e). catch (Exception $e) chỉ bắt ngoại lệ, không bắt lỗi của engine như TypeError, ValueError hay DivisionByZeroError, vốn kế thừa từ Error. Cả Exception và Error đều cài đặt Throwable.

Làm sao để bắt nhiều ngoại lệ trong một khối catch của PHP?

Ngăn cách các kiểu bằng dấu gạch đứng: catch (InvalidArgumentException | RangeException $e). Bạn cũng có thể viết nhiều khối catch nối tiếp nhau; PHP dùng khối đầu tiên khớp, nên hãy đặt các kiểu cụ thể nhất lên trước.

finally có chạy khi có return trong try không?

Có. finally chạy sau try và catch dù code đã trả về, ném ra ngoại lệ hay kết thúc bình thường, kể cả khi try có return. Nếu chính finally trả về một giá trị, giá trị đó sẽ thay cho giá trị từ try, nên hãy tránh return bên trong finally.

try catch có bắt được cảnh báo (warning) trong PHP không?

Không. Cảnh báo và thông báo (như đọc một khóa không tồn tại của mảng) không phải là ngoại lệ, nên catch không bao giờ thấy chúng. Hãy kiểm tra điều kiện trước khi gọi hàm, dùng chế độ ném ngoại lệ của hàm nếu có (json_decode(..., flags: JSON_THROW_ON_ERROR)), hoặc chuyển cảnh báo thành ngoại lệ bằng set_error_handler() và ErrorException.

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