在 PHP 中用 throw new Exception('message'); 抛出异常。程序在这一行停止,跳转到最近的、能接受该异常类型的 catch 块,在那里 $e->getMessage() 返回异常信息。使用 InvalidArgumentException 这样的具体类,调用方就能区分不同的问题。
<?php
function setQuantity(int $qty): string
{
if ($qty < 1) {
throw new InvalidArgumentException("Quantity must be at least 1, got $qty");
}
return "Quantity set to $qty";
}
try {
echo setQuantity(3), "\n";
echo setQuantity(0), "\n";
} catch (InvalidArgumentException $e) {
echo 'Rejected: ', $e->getMessage(), "\n";
}
异常是一个对象。throw 把它沿调用栈向上传递,直到第一个有匹配 catch 的调用方,由它决定怎么处理;发现问题的函数不需要知道这些。捕获的机制、finally 以及捕获多种类型,见 try catch 页面。
异常对象包含什么
每个异常都带有一条信息、一个整数代码、创建它的文件和行号,以及调用栈跟踪。构造函数接受 ($message, $code, $previous),都是可选的:
<?php
function findOrder(int $id): array
{
throw new RuntimeException("Order $id not found", 404);
}
try {
findOrder(77);
} catch (RuntimeException $e) {
echo 'Class: ', get_class($e), "\n";
echo 'Message: ', $e->getMessage(), "\n";
echo 'Code: ', $e->getCode(), "\n";
echo 'File: ', basename($e->getFile()), "\n";
echo 'Line: ', $e->getLine(), "\n";
echo "Trace:\n", $e->getTraceAsString(), "\n";
}
getLine() 是 throw new 所在的行,而不是 catch 所在的行。跟踪信息列出了走到这里所经过的调用(#0 /home/index.php(8): findOrder()),日志中出现错误时,通常首先要看的就是它。把异常转换成字符串(echo $e; 或 (string) $e)会以 PHP 的标准格式输出全部信息。
内置异常类
PHP 在标准 PHP 库(SPL)中提供了一系列异常类。抛出能说明问题的那个类,代码更易读,调用方也能精确捕获。下面这些类都继承自 Exception:
| 类 | 什么时候抛出 |
|---|
InvalidArgumentException | 参数形式不对:名称为空、未知的选项 |
DomainException | 值超出了合理的范围:价格为负数 |
OutOfRangeException | 代码请求了一个永远不可能存在的索引(调用方的 bug) |
OutOfBoundsException | 只有运行时才知道的数据中缺少某个键或索引 |
RangeException | 运行时计算出的结果超出了有效范围 |
LengthException | 某个东西太长或太短 |
RuntimeException | 只有运行时才能发现的问题:磁盘已满、超时 |
UnexpectedValueException | 函数返回或接收到了类型出乎意料的值 |
LogicException | 代码本身有误,是 bug 而不是输入有问题 |
JsonException | 使用 JSON_THROW_ON_ERROR 时由 json_encode()/json_decode() 抛出 |
InvalidArgumentException、DomainException、LengthException 和 OutOfRangeException 继承自 LogicException;OutOfBoundsException、RangeException 和 UnexpectedValueException 继承自 RuntimeException。你可以自己查看任何类的父类:
<?php
foreach ([InvalidArgumentException::class, RangeException::class, JsonException::class, TypeError::class] as $class) {
echo $class, ' -> ', implode(' -> ', class_parents($class)), "\n";
}
$e = new OutOfRangeException('Index 9 is out of range');
var_dump($e instanceof LogicException, $e instanceof RuntimeException, $e instanceof Throwable);
TypeError 的父类是 Error,不是 Exception:它属于下面要讲的第二个家族。
PHP 自身抛出的错误
从 PHP 7 起,尤其是 PHP 8 以后,PHP 自己的函数和运算符遇到以前只是警告的问题时,会抛出 Error 的子类。捕获方式相同,但 catch (Exception $e) 匹配不到它们:
<?php
declare(strict_types=1);
$cases = [
'intdiv by zero' => fn () => intdiv(10, 0),
'modulo by zero' => fn () => 7 % 0,
'wrong arg type' => fn () => str_repeat(5, 2),
'invalid value' => fn () => array_fill(0, -1, 'x'),
'too few args' => fn () => (fn (int $a, int $b) => $a + $b)(1),
'no match arm' => fn () => match (99) { 1 => 'one', 2 => 'two' },
'undefined func' => fn () => undefined_function(),
];
foreach ($cases as $label => $run) {
try {
$run();
} catch (Error $e) {
printf("%-16s %-22s %s\n", $label, get_class($e), $e->getMessage());
}
}
DivisionByZeroError、TypeError、ValueError、ArgumentCountError、UnhandledMatchError 以及普通的 Error 都继承自 Error。它们往往说明调用代码有 bug,而不是数据有问题,所以被放在 Exception 体系之外。正是 declare(strict_types=1) 让 str_repeat(5, 2) 产生 TypeError;没有它,PHP 会把 5 转换成 "5"。
编写自定义异常类
通常一行子类就够了:类名本身就表达了含义,调用方可以精确捕获这种类型。捕获方需要数据(哪个订单、多少金额)时,就添加属性,并用 parent::__construct() 把信息传给父类:
<?php
class PaymentException extends RuntimeException {}
class InsufficientFundsException extends PaymentException
{
public function __construct(
public readonly int $balanceCents,
public readonly int $neededCents,
) {
parent::__construct(sprintf(
'Insufficient funds: balance %.2f, needed %.2f',
$balanceCents / 100,
$neededCents / 100,
), 402);
}
public function shortfall(): int
{
return $this->neededCents - $this->balanceCents;
}
}
function pay(int $balance, int $amount): string
{
if ($amount > $balance) {
throw new InsufficientFundsException($balance, $amount);
}
return 'paid';
}
try {
echo pay(5000, 1200), "\n";
echo pay(5000, 8000), "\n";
} catch (InsufficientFundsException $e) {
echo $e->getMessage(), "\n";
echo 'Top up at least ', $e->shortfall() / 100, " EUR (code {$e->getCode()})\n";
} catch (PaymentException $e) {
echo 'Payment failed: ', $e->getMessage(), "\n";
}
选父类时要谨慎:继承 RuntimeException(而不是普通的 Exception)意味着捕获 RuntimeException 的代码也会处理你的异常。建立一个小的层级结构,每个库或模块一个基础异常,下面再分具体的子类,调用方就可以在“捕获支付模块的所有异常”和“只捕获这一种情况”之间选择。
用 previous 串联异常
捕获一个底层异常、再抛出一个更高层的异常时,把原来的异常作为第三个参数传入。这样什么都不会丢失:getPrevious() 可以一路回溯到根本原因。
<?php
class ImportException extends RuntimeException {}
function parseRow(string $line, int $n): array
{
$cols = str_getcsv($line);
if (count($cols) !== 3) {
throw new UnexpectedValueException("row $n has " . count($cols) . ' columns, expected 3');
}
return $cols;
}
function import(array $lines): int
{
foreach ($lines as $i => $line) {
try {
parseRow($line, $i + 1);
} catch (UnexpectedValueException $e) {
throw new ImportException('customers.csv could not be imported', 0, $e);
}
}
return count($lines);
}
try {
import(['Ada,ada@example.com,UK', 'Linus,linus@example.com']);
} catch (ImportException $e) {
$level = 0;
for ($x = $e; $x !== null; $x = $x->getPrevious()) {
echo str_repeat(' ', $level++), get_class($x), ': ', $x->getMessage(), "\n";
}
}
用户看到的是“customers.csv could not be imported”;日志顺着异常链,还能拿到“row 2 has 2 columns”。如果串联的异常一直没有被捕获,PHP 的致命错误会输出整条链:先是最初的原因,然后是每一层包装,各自位于一行 Next 之下。
作为表达式的 throw
从 PHP 8.0 起,throw 是一个表达式,所以可以出现在任何需要值的地方:?? 之后、三元运算中、箭头函数里。“取到这个值,否则失败”就能写成一行:
<?php
$config = ['host' => 'localhost', 'port' => '5432'];
function setting(array $config, string $key): string
{
return $config[$key] ?? throw new OutOfBoundsException("Missing setting '$key'");
}
echo setting($config, 'host'), "\n";
$port = (int) $config['port'] ?: throw new DomainException('Port must not be 0');
echo $port, "\n";
$positive = fn (int $n) => $n > 0 ? $n : throw new DomainException("$n is not positive");
echo $positive(8), "\n";
try {
echo setting($config, 'password'), "\n";
} catch (OutOfBoundsException $e) {
echo $e->getMessage(), "\n";
}
什么时候不该抛出异常
异常用于当前函数无法处理、正常流程也预料不到的情况。搜索没有结果、可选字段为空、用户没有登录,这些都是普通的结果:返回 null、false 或空数组,让调用方去检查;只有在继续运行会出错的情况下才使用异常。
<?php
$users = ['ada' => 'Ada Lovelace', 'linus' => 'Linus Torvalds'];
// Ordinary outcome: "not found" is a normal answer, so return null
function findUser(array $users, string $handle): ?string
{
return $users[$handle] ?? null;
}
// Broken input: an empty handle is a bug in the caller, so throw
function requireHandle(string $handle): string
{
if ($handle === '') {
throw new InvalidArgumentException('Handle must not be empty');
}
return strtolower($handle);
}
foreach (['ADA', 'grace', ''] as $input) {
try {
$name = findUser($users, requireHandle($input));
echo $input, ': ', $name ?? 'no such user', "\n";
} catch (InvalidArgumentException $e) {
echo 'Bug: ', $e->getMessage(), "\n";
}
}
还有两个习惯能让异常代码保持清晰:不要为了忽略异常而捕获它(空的 catch (Exception $e) {} 会把下一个真正的 bug 藏起来);抛出具体的类,而不是光秃秃的 Exception,这样调用方就不必为了处理一种情况而捕获所有异常。