在 PHP 中,你把可能出错的代码放进 try { ... },在 catch (Exception $e) { ... } 里处理失败。只要 try 代码块中有代码抛出异常,PHP 就直接跳到匹配的 catch,$e->getMessage() 会告诉你出了什么问题。如果没有抛出任何异常,catch 代码块会被跳过。
把 divide(10, 0) 改成 divide(10, 5),再运行一次:没有异常,所以 not printed 被打印出来,catch 代码块被跳过。如果没有 try,这个异常会以一个致命的“Uncaught”错误结束脚本。怎么抛出自己的异常、编写异常类,见异常页面。
try catch 的语法
完整的形式有三个部分。try 是必需的,另外至少要有一个 catch 或一个 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
}
catch 里的类型决定了它能捕获什么:这个类本身,以及所有继承它的类。从 PHP 8.0 开始,如果用不到变量,可以省略它:catch (JsonException) { ... }。
捕获 Exception、Error 还是 Throwable
从 PHP 7 开始,PHP 自己检测到的失败(调用不存在的方法、参数类型错误、除以零)抛出的是 Error,而不是 Exception;PHP 8 还把许多原来的警告改成了 TypeError 和 ValueError。两者都实现了 Throwable 接口,但谁也不继承谁,所以 catch (Exception $e) 捕获不到 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, ...
运行下面的代码,看看每个问题由哪个 catch 处理:
那么应该捕获哪一个?
- 当你知道可能出什么错、出错后该怎么办时,用
catch (SomeSpecificException $e)。大多数代码都是这种情况。 - 用
catch (Exception $e)处理“库或我的代码有意抛出的任何异常”。 - 在最外层(请求处理器、任务运行器)用
catch (Throwable $e)记录每一个失败,包括 PHP 自身的错误,并显示一条友好的提示。
捕获多种异常类型
写多个 catch 代码块,用不同的方式处理不同的类型。PHP 从上到下依次尝试,使用第一个匹配的,所以具体的类型必须放在它的父类前面,否则父类会先把所有异常都捕获掉。当几种类型的处理方式相同时,用 | 把它们合并到一个 catch 里:
这三个类都继承自 Exception,所以一个 catch (Exception $e) 也能捕获它们,但那样代码就没法对其中一种返回 404、对其他的返回 400 了。
finally:总会运行的代码
finally 代码块在 try 和 catch 之后运行,不管它们是怎么结束的:正常结束、异常被捕获、异常没人捕获,或者执行了 return。它是释放 try 代码块所获取资源的地方:关闭文件、释放锁、停止计时器。
对于 good.txt,文件在打印 saved 之前就关闭了,因为 finally 会在函数真正返回之前运行。对于 bad.txt,process() 里没有 catch:finally 关闭文件,然后异常继续向上传递到调用者的 catch。
不要在 finally 里写 return:它会替换 try 代码块返回的值,还会悄悄丢掉正在向外抛出的异常:
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
重新抛出或包装异常
有时 catch 代码块只能处理问题的一部分:记录日志、清理资源,然后让异常继续传递。throw $e; 会重新抛出同一个异常。更常见的做法是在保留原始原因的同时补充上下文,构造函数的第三个参数 $previous 就是为此准备的:
调用者只需处理一个 ConfigException,不需要知道配置是 JSON 格式,而 getPrevious() 为日志保留了底层的细节。
未捕获的异常与全局处理函数
没有任何 catch 处理的异常会以致命错误结束脚本,错误信息包括类名、消息、文件和行号,以及调用栈:
<?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 打印 first charge ok,然后输出:
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
在线上网站,这条信息永远不应该让访问者看到。set_exception_handler() 注册一个函数,接收所有没人捕获的异常,这样就有一个统一的地方记录日志并显示一个礼貌的页面:
处理函数运行完后脚本就停止了;throw 之后无法再继续执行。PHP 自己的错误文本会不会显示在页面上,由 display_errors 控制,详见错误报告。
警告不是异常
try/catch 只能看到被抛出的东西。许多较老的 PHP 函数不会抛出异常:它们返回 false 或 null,最多打印一条警告。json_decode() 就是典型的例子,在你要求它抛出异常之前,把它包在 try 里不会有任何作用:
对于没有这种选项的函数,检查返回值(if ($handle === false)),或者先检查前提条件(file_exists()、isset())。如果你想用一条规则处理所有情况,可以写一个抛出 ErrorException 的 set_error_handler(),把每个警告都变成真正的异常。
实用示例:用异常校验表单输入
一个实用的模式:校验函数抛出异常,页面捕获异常并把消息显示在表单旁边。运行它,然后在表单里输入一个年龄并点击 Send:同一个脚本会带着填好的 $_POST 再运行一次。
试试空字段、twelve、9 和 30。每条不通过的规则都会抛出自己的消息,而一个 catch 把它们都变成同样的一行红字。表单页面在此基础上扩展到多个字段,并在每个字段旁边显示错误。
常见问题
PHP 中 try catch 是怎么工作的?
PHP 运行 try { } 里的代码。如果其中有代码抛出异常,PHP 跳过 try 代码块剩下的部分,运行第一个类型匹配的 catch 代码块,抛出的对象就在变量里:catch (Exception $e) { echo $e->getMessage(); }。如果没有抛出任何异常,catch 代码块会被跳过。
PHP 中怎么捕获所有的异常和错误?
捕获 Throwable:catch (Throwable $e)。catch (Exception $e) 只能捕获异常,捕获不到 TypeError、ValueError 或 DivisionByZeroError 这样的引擎错误,它们继承自 Error。Exception 和 Error 都实现了 Throwable。
PHP 中怎么在一个 catch 代码块里捕获多种异常?
用竖线分隔类型:catch (InvalidArgumentException | RangeException $e)。你也可以一个接一个地写多个 catch 代码块;PHP 使用第一个匹配的,所以要把最具体的类型放在前面。
try 里有 return 时,finally 还会运行吗?
会。无论代码是返回了、抛出了异常还是正常结束,finally 都会在 try 和 catch 之后运行,try 里包含 return 时也一样。如果 finally 自己返回了一个值,这个值会替换 try 返回的值,所以不要在 finally 里写 return。
PHP 的 try catch 能捕获警告吗?
不能。警告和通知(比如读取不存在的数组键)不是异常,所以 catch 永远看不到它们。在调用前先检查条件;如果函数有异常模式,就开启它(json_decode(..., flags: JSON_THROW_ON_ERROR));或者用 set_error_handler() 和 ErrorException 把警告转换成异常。