try catch 是 PHP 接住執行期錯誤的主要方式:把可能出錯的程式碼放進 try,用 catch 依例外類型決定怎麼回應,必要時用 finally 做清理。這篇從 php try catch 最小語法出發,一路講到 Throwable、自訂 Exception、PDO 實戰與常見踩坑。

讀完後,你應該能寫出可運作的 try catch finally、分辨 ExceptionError 的差異,並知道哪些函式失敗時不會自動拋例外。

語法速查

try {
	// 可能拋出例外的程式碼
} catch (SpecificException $e) {
	// 處理特定例外
} catch (Throwable $e) {
	// 兜底:任何 Error 或 Exception
} finally {
	// 無論成功失敗都會執行(關連線、釋放資源)
}
關鍵字用途
throw new Exception('msg')主動拋出例外
catch (Type $e)依類型接住
finally清理資源(可省略)
ThrowablePHP 7+ 同時涵蓋 ErrorException

PHP try catch 最小範例

下面是最常見的寫法:在 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); // 5
} catch (InvalidArgumentException $e) {
	echo '輸入無效:' . $e->getMessage();
}

throw 之後,同一區塊中尚未執行的程式碼會被跳過,PHP 會往外尋找第一個符合類型的 catch。若一路找不到,例外就會繼續往呼叫堆疊上層傳遞,最後可能成為未捕獲例外。

加上 finally 後,無論成功或失敗都會執行清理:

<?php

try {
	echo divide(10, 2);
} catch (InvalidArgumentException $e) {
	echo '輸入無效:' . $e->getMessage();
} finally {
	echo PHP_EOL . '這段清理流程一定會執行。';
}

Exception、Error 與 Throwable 有什麼差?

PHP 中可以被 throw 拋出、再由 catch 接住的物件,必須實作 Throwable 介面:

類型代表什麼catch (Exception $e) 能接住嗎?
Exception應用程式或函式庫預期可處理的例外
ErrorPHP 執行期錯誤(如型別錯誤、部分算術錯誤)
Throwable兩者共同的上層介面✅(兩者皆可)

因此,catch (Exception $e) 不會接住 Error。若需要同時處理兩者,可以捕獲 Throwable;不過,這不代表所有錯誤都適合被吃掉。語法錯誤、設定問題或程式設計錯誤,通常應該在開發階段被找出,而不是在生產環境偷偷假裝沒事。

實務建議:

  • 業務邏輯:優先 catch 具體的 Exception 子類別。
  • 應用程式邊界(API 回應、全域錯誤處理):可考慮 catch (Throwable $e) 記錄未預期錯誤。

檔案操作不一定會自動拋出例外

這是初學者很容易踩到的坑:並非所有 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

當程式只丟出一個通用的 Exception,呼叫端往往只能看訊息猜發生什麼事。建立有語意的例外類別,可以讓 catch 更精準,也讓日後搜尋記錄檔容易很多。

<?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 '設定檔不存在,請檢查部署內容。';
}

如果需要保留較低層的原因,可以在建立新例外時傳入上一個例外(例外鏈結):

<?php

class DatabaseException extends RuntimeException
{
}

try {
	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 '發生未預期的錯誤。';
}

若不同例外需要完全相同的處理方式,也可以使用聯合捕獲語法(PHP 8.0+):

<?php

try {
	// 可能拋出這兩類例外
} catch (InvalidArgumentException | UnexpectedValueException $exception) {
	echo '收到無效的資料。';
}

實用案例:包裝 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() 的資訊
}

這個範例有三個值得保留的設計:

  1. addBook() 將底層的 PDOException 包裝成應用程式看得懂的 DatabaseException
  2. throw new DatabaseException(..., 0, $exception) 保留原始例外,方便記錄與除錯。
  3. 對外只顯示安全、友善的訊息,不把資料庫細節直接送到瀏覽器。

常見錯誤與排解

try catch 接不到錯誤?

症狀可能原因解法
程式直接 Fatal error語法錯誤、記憶體不足try catch 接不住,需在開發階段修正
catch (Exception) 沒反應實際拋出的是 Error改用 catch (Throwable $e) 或修正型別錯誤
函式失敗但沒進 catch函式回傳 false 而非拋例外檢查回傳值後自行 throw
PDO 沒拋 PDOException未設定 ERRMODE_EXCEPTION設定 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION

RuntimeException vs Exception 怎麼選?

RuntimeExceptionException 的子類別,代表「執行時發現的、通常可透過程式修正的問題」。PHP 內建還有 LogicException(程式邏輯錯誤)等子類別。自訂例外時,繼承最接近語意的內建類別即可;不必為了「看起來專業」而一律繼承 Exception

finally 裡可以 return 嗎?

技術上可以,但 finally 中的 return 可能覆蓋 trycatch 原本要回傳的值,讓除錯難度升級。清理區塊應保持簡單,避免在 finallyreturn 或再次拋出無關例外。

常見問題

catch (Exception $exception) 能接住所有錯誤嗎?

不能。Error 不繼承 Exception。若需要同時接住實作 ThrowableExceptionError,可以使用 catch (Throwable $exception),但應先確認這樣做符合你的錯誤處理策略。

所有 PHP 函式失敗時都會拋出例外嗎?

不會。不同函式的行為不同,有些會回傳 falsenull 或錯誤碼,有些才會拋出例外。請先查閱該函式的文件,再決定是否需要檢查回傳值或設定錯誤模式。

finally 一定會執行嗎?

在正常離開 trycatch 的流程中,finally 會執行,包括 trycatch 中遇到 return 的情況。但若程式直接終止(如 exit),或 finally 自己拋出例外,就可能改變原本的流程與結果。

總結

PHP 例外處理的核心不是「把所有錯誤抓起來」,而是讓錯誤在合適的邊界被辨識、記錄與回應。先分清楚 ExceptionErrorThrowable,再用具體的自訂例外表達意圖;需要清理時使用 finally,需要跨層轉換時保留 getPrevious() 的原始脈絡。

下一步可繼續閱讀 PHP 類別與物件入門存取修飾符

參考資料

  1. PHP 官方文件:Exceptions:說明 trycatchfinally、例外傳遞與 Throwable
  2. PHP 官方文件:Errors in PHP 7:說明 ErrorException 的差異,以及 Throwable 的角色。
  3. PHP 官方文件:PDO — Errors and error handling:說明 PDO 的錯誤模式與 PDOException