例外處理有點像替程式準備安全網:希望它永遠用不到,但真的出事時,總不能只對著錯誤畫面喝咖啡。這篇會用白話說明 PHP 的 try、catch、finally、Throwable 與自訂例外,讓你知道該在哪裡接住問題,也知道哪些問題根本不是同一種球。
讀完後,你應該能寫出基本的例外處理、依例外類型採取不同策略,並在資料庫操作中保留原始錯誤脈絡,而不是把所有錯誤都包成一句「好像哪裡怪怪的」。
目錄
先分清楚:例外、錯誤與 Throwable
PHP 中可以被 throw 拋出、再由 catch 接住的物件,必須實作 Throwable 介面。內建的 Exception 與 Error 都屬於這個家族,但兩者的繼承關係不同:
Exception通常代表應用程式或函式庫預期可以處理的例外。Error代表 PHP 執行期間發現的錯誤,例如部分型別或算術錯誤。Throwable是兩者共同的上層介面。
因此,catch (Exception $e) 不會接住 Error。如果你確實需要同時處理兩者,可以捕獲 Throwable;不過,這不代表所有錯誤都適合被吃掉。語法錯誤、設定問題或程式設計錯誤,通常應該在開發與記錄階段被找出,而不是在生產環境偷偷假裝沒事。
try、catch 與 finally 怎麼合作?
這三個關鍵字各自負責一件事:
try:放入可能拋出例外的程式碼。catch:接住符合指定類型的Throwable,並決定如何回應。finally:不論是否發生例外,都在流程離開try/catch前執行清理工作。
一個最小但完整的例子如下:
<?php
function divide(int $dividend, int $divisor): float
{
if ($divisor === 0) {
throw new InvalidArgumentException('除數不能是 0。');
}
return $dividend / $divisor;
}
try {
echo divide(10, 2);
} catch (InvalidArgumentException $exception) {
echo '輸入無效:' . $exception->getMessage();
} finally {
echo PHP_EOL . '這段清理流程一定會執行。';
}
throw 之後,同一個區塊中尚未執行的程式碼會被跳過,PHP 會往外尋找第一個符合類型的 catch。如果一路找不到,例外就會繼續往呼叫堆疊上層傳遞,最後可能成為未捕獲例外。
另外,finally 不是「錯誤發生時才執行」的區塊;成功與失敗都會走到它。它很適合放關閉檔案、釋放鎖定或還原暫存狀態等清理工作。清理區塊也不要再次拋出無關例外,否則可能覆蓋原本更重要的錯誤——錯誤會排隊,但不保證乖乖排隊。
檔案操作不一定會自動拋出例外
這是初學者很容易踩到的坑:並非所有 PHP 函式失敗時都會拋出 Exception。例如 fopen() 找不到檔案時,通常會回傳 false 並產生警告;單純把它放進 try,不會自動變成可捕獲的例外。
如果你的程式需要用例外流程處理這種失敗,就要明確檢查回傳值,再自行拋出例外:
<?php
function openFile(string $path)
{
$handle = fopen($path, 'r');
if ($handle === false) {
throw new RuntimeException('無法開啟檔案:' . $path);
}
return $handle;
}
$handle = null;
try {
$handle = openFile(__DIR__ . '/example.txt');
$content = stream_get_contents($handle);
} catch (RuntimeException $exception) {
echo '檔案處理失敗:' . $exception->getMessage();
} finally {
if (is_resource($handle)) {
fclose($handle);
}
}
這裡的重點不是把每個警告都硬轉成例外,而是先確認你使用的 API 如何回報失敗,再選擇一致的處理方式。
建立自訂例外
當程式只丟出一個通用的 Exception,呼叫端往往只能看訊息猜發生什麼事。建立有語意的例外類別,可以讓 catch 更精準,也讓日後搜尋記錄檔容易很多。
自訂例外通常只要繼承 Exception 或它的子類別即可:
<?php
class FileNotFoundException extends RuntimeException
{
}
function readConfig(string $path): string
{
if (!is_file($path)) {
throw new FileNotFoundException('找不到設定檔:' . $path);
}
$config = file_get_contents($path);
if ($config === false) {
throw new RuntimeException('無法讀取設定檔:' . $path);
}
return $config;
}
try {
$config = readConfig(__DIR__ . '/config.php');
} catch (FileNotFoundException $exception) {
echo '設定檔不存在,請檢查部署內容。';
}
類別名稱本身就是文件。與其在每個地方解析一長串錯誤訊息,不如讓 FileNotFoundException 直接說明「是哪一類問題」。
如果需要保留較低層的原因,可以在建立新例外時傳入上一個例外。這種做法稱為例外鏈結:
<?php
class DatabaseException extends RuntimeException
{
}
try {
// 這裡假設資料庫函式庫拋出了 PDOException。
throw new PDOException('連線被拒絕');
} catch (PDOException $exception) {
throw new DatabaseException('無法完成資料庫操作。', 0, $exception);
}
上層可以用 getMessage() 取得對使用者較安全的訊息,也可以用 getPrevious() 追查原始原因。請避免直接把資料庫帳號、密碼、完整 SQL 或堆疊資訊顯示給使用者;這些內容比較適合寫入受保護的伺服器記錄。
多個 catch:順序就是優先權
一個 try 後面可以接多個 catch。PHP 會從上到下比對,執行第一個符合的區塊。因此,具體的例外要放前面,較一般的類型放後面:
<?php
try {
// 可能拋出不同類型的 Throwable。
} catch (FileNotFoundException $exception) {
echo '找不到檔案。';
} catch (RuntimeException $exception) {
echo '執行時發生可預期的問題。';
} catch (Throwable $exception) {
echo '發生未預期的錯誤。';
}
反過來,如果先寫 catch (Throwable $exception),後面的區塊就永遠沒有機會接手。這就像先在門口安排一個「所有人都進來」的保全,後面再安排「只接待 PHP 開發者」的櫃檯,後者自然會失業。
若不同例外需要完全相同的處理方式,也可以使用聯合捕獲語法:
<?php
try {
// 可能拋出這兩類例外。
} catch (InvalidArgumentException | UnexpectedValueException $exception) {
echo '收到無效的資料。';
}
finally 的實務用法
finally 適合處理「無論成功或失敗都要做」的事情,例如關閉檔案、釋放鎖或還原狀態。它不適合拿來掩蓋錯誤,也不建議在裡面寫 return,因為那可能覆蓋 try 或 catch 原本要回傳的值,讓除錯難度直接升級。
需要注意的是,finally 不是萬能清潔工。若程式直接結束,或清理本身拋出新的例外,流程可能不會照你預想的方式完成。因此,清理程式應該保持簡單、可預期。
實用案例:包裝 PDO 例外
以下用新增書籍的流程示範例外分層。範例明確設定 PDO 使用例外模式,避免把不同的錯誤回報方式混在一起:
<?php
class DatabaseException extends RuntimeException
{
}
function addBook(PDO $db, string $title, string $author): void
{
try {
$statement = $db->prepare(
'INSERT INTO books (title, author) VALUES (:title, :author)'
);
$statement->execute([
'title' => $title,
'author' => $author,
]);
} catch (PDOException $exception) {
throw new DatabaseException('新增書籍失敗。', 0, $exception);
}
}
try {
$db = new PDO(
'mysql:host=localhost;dbname=library;charset=utf8mb4',
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]
);
addBook($db, '1984', 'George Orwell');
echo '書籍新增成功。';
} catch (DatabaseException $exception) {
echo '目前無法新增書籍,請稍後再試。';
// 實務上可在這裡記錄 $exception 與 getPrevious() 的資訊。
}
這個範例有三個值得保留的設計:
addBook()將底層的PDOException包裝成應用程式看得懂的DatabaseException。throw new DatabaseException(..., 0, $exception)保留原始例外,方便記錄與除錯。- 對外只顯示安全、友善的訊息,不把資料庫細節直接送到瀏覽器。
資料庫連線的生命週期也不必硬塞進 finally。當 $db 離開作用域且沒有其他參照時,PHP 會回收物件;真正需要 finally 的情境,應該是你有明確的資源或狀態需要無條件清理。
常見問題
catch (Exception $exception) 能接住所有錯誤嗎?
不能。Error 不繼承 Exception。若需要同時接住實作 Throwable 的 Exception 與 Error,可以使用 catch (Throwable $exception),但應先確認這樣做符合你的錯誤處理策略。
所有 PHP 函式失敗時都會拋出例外嗎?
不會。不同函式的行為不同,有些會回傳 false、null 或錯誤碼,有些才會拋出例外。請先查閱該函式的文件,再決定是否需要檢查回傳值或設定錯誤模式。
finally 一定會執行嗎?
在正常離開 try/catch 的流程中,finally 會執行,包括 try 或 catch 中遇到 return 的情況。但若程式直接終止,或 finally 自己拋出例外,就可能改變原本的流程與結果。
什麼時候該捕獲 Throwable?
在應用程式邊界、記錄未預期錯誤或建立統一錯誤回應時,Throwable 可能有用。一般業務邏輯則應優先捕獲具體類型,否則很容易把真正的程式錯誤當成正常流程處理。
總結
PHP 例外處理的核心不是「把所有錯誤抓起來」,而是讓錯誤在合適的邊界被辨識、記錄與回應。先分清楚 Exception、Error 與 Throwable,再用具體的自訂例外表達意圖;需要清理時使用 finally,需要跨層轉換時保留 getPrevious() 的原始脈絡。
當你開始這樣設計,錯誤就不再只是紅色畫面,而會變成可以追蹤、可以處理的訊號。咖啡可以繼續喝,例外則交給正確的 catch。
參考資料
- PHP 官方文件:Exceptions:說明
try、catch、finally、例外傳遞與Throwable。 - PHP 官方文件:Errors in PHP 7:說明
Error與Exception的差異,以及Throwable的角色。 - PHP 官方文件:PDO — Errors and error handling:說明 PDO 的錯誤模式與
PDOException。

