PHP Exception
try/catch/finally, custom exceptions, exception chaining, and when to throw.
Basic structure
<?php
function divide(float $a, float $b): float {
if ($b == 0.0) {
throw new InvalidArgumentException("除数不能为 0");
}
return $a / $b;
}
try {
echo divide(10, 4), "\n";
echo divide(10, 0), "\n";
echo "这行不会执行\n";
} catch (InvalidArgumentException $e) {
echo "捕获: " . $e->getMessage(), "\n";
} finally {
echo "finally 总会执行\n";
}2.5
捕获: 除数不能为 0
finally 总会执行After throw the function ends immediately and control passes to the nearest matching catch. finally always runs — good for cleanup.
Multiple catch blocks
catch blocks match top-down, so put subclasses before parents. Leading with Exception disables everything below.
<?php
class ValidationException extends Exception {}
class NotFoundException extends Exception {}
function run(string $kind): void {
match ($kind) {
"v" => throw new ValidationException("字段非法"),
"n" => throw new NotFoundException("记录不存在"),
default => throw new RuntimeException("未知错误"),
};
}
foreach (["v", "n", "x"] as $k) {
try {
run($k);
} catch (ValidationException $e) {
echo "校验失败: " . $e->getMessage() . "\n";
} catch (NotFoundException $e) {
echo "找不到: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "其它: " . $e->getMessage() . "\n";
}
}校验失败: 字段非法
找不到: 记录不存在
其它: 未知错误Custom exceptions
Custom exceptions can carry structured data instead of concatenated strings.
<?php
class ValidationException extends Exception
{
public function __construct(private array $errors)
{
parent::__construct("校验失败: " . implode(", ", $errors));
}
public function errors(): array
{
return $this->errors;
}
}
try {
throw new ValidationException(["name 不能为空", "email 格式错误"]);
} catch (ValidationException $e) {
echo $e->getMessage(), "\n";
print_r($e->errors());
echo $e->getCode(), "\n";
}校验失败: name 不能为空, email 格式错误
Array
(
[0] => name 不能为空
[1] => email 格式错误
)
0getCode() defaults to 0; pass a business code as the second constructor argument. Note Exception::$code must be an int.
Exception chaining
Never swallow a low-level exception — wrap it and pass it as previous to keep the chain.
<?php
class RepositoryException extends RuntimeException {}
function findUser(int $id): array {
try {
if ($id !== 1) {
throw new PDOException("SQLSTATE[HY000] 连接超时");
}
return ["id" => 1, "name" => "Tom"];
} catch (PDOException $e) {
throw new RepositoryException("查询用户失败: id=$id", 0, $e);
}
}
try {
findUser(2);
} catch (RepositoryException $e) {
echo $e->getMessage(), "\n";
echo "原因: " . $e->getPrevious()->getMessage(), "\n";
}查询用户失败: id=2
原因: SQLSTATE[HY000] 连接超时previous. Without it the caller only sees "query failed" — never the real cause.Throw or return
| Situation | Approach |
|---|---|
| 调用方无法继续(依赖缺失、配置错误) | 抛异常 |
| 可预期的业务分支(找不到、余额不足) | 抛异常或返回结果对象 |
| 用户输入校验 | 收集错误数组,不抛 |
| 「没有更多数据」 | 返回空数组 / null |
| 程序 bug(不可能的状态) | 抛 LogicException |
A practical rule: exceptions for the shouldn't-happen, return values for normal branches. Using exceptions for control flow forces try everywhere.
Never swallow
<?php
// 反例:吞掉异常,问题被掩盖
try {
throw new RuntimeException("磁盘写满");
} catch (Exception $e) {
// 什么都不做
}
echo "反例:异常被吞,程序以为一切正常\n";
// 正例:记录后重新抛出,或转成业务异常
try {
throw new RuntimeException("磁盘写满");
} catch (Exception $e) {
error_log("写入失败: " . $e->getMessage());
echo "正例:已记录\n";
}反例:异常被吞,程序以为一切正常
正例:已记录catch (Exception $e) {} deserves a review rejection. Handle it, log and rethrow, or don't catch. Empty catches turn bugs into "intermittent, cannot reproduce".基本结构
<?php
function divide(float $a, float $b): float {
if ($b == 0.0) {
throw new InvalidArgumentException("除数不能为 0");
}
return $a / $b;
}
try {
echo divide(10, 4), "\n";
echo divide(10, 0), "\n";
echo "这行不会执行\n";
} catch (InvalidArgumentException $e) {
echo "捕获: " . $e->getMessage(), "\n";
} finally {
echo "finally 总会执行\n";
}2.5
捕获: 除数不能为 0
finally 总会执行throw 之后当前函数立即终止,控制权交给最近的匹配 catch。finally 无论是否抛异常都会跑,适合释放资源。
多个 catch
catch 从上往下匹配,子类要写在父类前面。把 Exception 放最前面会让后面所有分支失效。
<?php
class ValidationException extends Exception {}
class NotFoundException extends Exception {}
function run(string $kind): void {
match ($kind) {
"v" => throw new ValidationException("字段非法"),
"n" => throw new NotFoundException("记录不存在"),
default => throw new RuntimeException("未知错误"),
};
}
foreach (["v", "n", "x"] as $k) {
try {
run($k);
} catch (ValidationException $e) {
echo "校验失败: " . $e->getMessage() . "\n";
} catch (NotFoundException $e) {
echo "找不到: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "其它: " . $e->getMessage() . "\n";
}
}校验失败: 字段非法
找不到: 记录不存在
其它: 未知错误自定义异常
自定义异常能携带结构化数据,比在消息里拼字符串好得多。
<?php
class ValidationException extends Exception
{
public function __construct(private array $errors)
{
parent::__construct("校验失败: " . implode(", ", $errors));
}
public function errors(): array
{
return $this->errors;
}
}
try {
throw new ValidationException(["name 不能为空", "email 格式错误"]);
} catch (ValidationException $e) {
echo $e->getMessage(), "\n";
print_r($e->errors());
echo $e->getCode(), "\n";
}校验失败: name 不能为空, email 格式错误
Array
(
[0] => name 不能为空
[1] => email 格式错误
)
0getCode() 默认是 0,需要业务错误码就在构造时传第二个参数。注意 Exception 的 $code 只能是 int。
异常链
底层异常不要吞掉,包装后作为 previous 传上去,堆栈才完整。
<?php
class RepositoryException extends RuntimeException {}
function findUser(int $id): array {
try {
if ($id !== 1) {
throw new PDOException("SQLSTATE[HY000] 连接超时");
}
return ["id" => 1, "name" => "Tom"];
} catch (PDOException $e) {
throw new RepositoryException("查询用户失败: id=$id", 0, $e);
}
}
try {
findUser(2);
} catch (RepositoryException $e) {
echo $e->getMessage(), "\n";
echo "原因: " . $e->getPrevious()->getMessage(), "\n";
}查询用户失败: id=2
原因: SQLSTATE[HY000] 连接超时throw new X("msg", 0, $e) 的第三个参数就是 previous。少了它,上层只能看到「查询失败」,永远不知道是连接超时还是语法错误。什么时候抛、什么时候返回
| 场景 | 做法 |
|---|---|
| 调用方无法继续(依赖缺失、配置错误) | 抛异常 |
| 可预期的业务分支(找不到、余额不足) | 抛异常或返回结果对象 |
| 用户输入校验 | 收集错误数组,不抛 |
| 「没有更多数据」 | 返回空数组 / null |
| 程序 bug(不可能的状态) | 抛 LogicException |
一条实用原则:异常用于「不该发生但发生了」,返回值用于「正常的分支」。用异常做流程控制会让调用方到处 try,可读性崩塌。
别吞异常
<?php
// 反例:吞掉异常,问题被掩盖
try {
throw new RuntimeException("磁盘写满");
} catch (Exception $e) {
// 什么都不做
}
echo "反例:异常被吞,程序以为一切正常\n";
// 正例:记录后重新抛出,或转成业务异常
try {
throw new RuntimeException("磁盘写满");
} catch (Exception $e) {
error_log("写入失败: " . $e->getMessage());
echo "正例:已记录\n";
}反例:异常被吞,程序以为一切正常
正例:已记录catch (Exception $e) {} 是代码审查里最该被打回的写法。要么处理,要么记录后重抛,要么根本别 catch。 空 catch 让线上问题变成「偶发、无法复现」。