本文目录导读:

在PHP项目中,Throwable 是一个核心接口,它定义了所有可抛出的错误和异常的基础行为,理解它的层级和用途,对于编写健壮的错误处理代码至关重要。
Throwable 接口族谱
这是PHP 7+ 引入的关键变化,统一了错误(Error)和异常(Exception)的处理路径。
Throwable (Interface)
│
├── Exception (Class, implements Throwable)
│ ├── LogicException
│ │ ├── InvalidArgumentException
│ │ ├── LengthException
│ │ └── BadFunctionCallException (含 BadMethodCallException)
│ ├── RuntimeException
│ │ ├── OutOfBoundsException
│ │ ├── OverflowException
│ │ ├── UnderflowException
│ │ ├── RangeException
│ │ ├── UnexpectedValueException
│ │ └── PDOException (扩展了 RuntimeException)
│ └── ... (其他用户自定义或库扩展)
│
└── Error (Class, implements Throwable)
├── TypeError
├── ArgumentCountError
├── ArithmeticError
│ └── DivisionByZeroError
├── CompileError
│ └── ParseError
└── ValueError (PHP 8.0 新增)
Exception:用于处理业务逻辑或可恢复的运行时问题(如:数据库连接失败、用户输入验证失败、文件未找到),通常我们应该主动try...catch它们。Error:用于处理PHP引擎层面的严重错误,很多是不可恢复或不应该被捕获的(如:内存耗尽、类不存在、类型不匹配),但在某些场景下(如框架的优雅降级),捕获Error是有意义的。
为什么要区分 Exception 和 Error?
| 特性 | Exception | Error |
|---|---|---|
| 起源 | 用户代码或扩展主动抛出 | PHP引擎触发,通常由编程错误导致 |
| 可恢复性 | 通常是可恢复的(如重试连接) | 通常不可恢复(如调用未定义函数) |
| 捕获目的 | 处理预期的异常流程 | 记录错误日志,进行优雅的崩溃恢复 |
| 强制检查 | 不得强制 catch(除非使用 phpdoc) |
不应强制 catch,但可以捕获 |
核心原则:不要用
Exception处理引擎错误,反之亦然。Error代表程序有 bug(如:类型不对、参数不够),需要修复代码;Exception代表外部条件异常(如:网络超时、机器人撞墙),需要处理流程。
在项目中的正确实践
✅ 好的做法:捕获 Throwable 用于全局兜底
在框架的入口文件(如 index.php)或全局异常处理器中,捕获 Throwable 可以防止任何未被捕获的崩溃暴露给用户。
<?php
// 全局异常/错误处理器
set_exception_handler(function (Throwable $throwable) {
// 1. 记录所有错误和异常到日志
error_log(sprintf(
"[%s] %s in %s:%d\n%s",
get_class($throwable),
$throwable->getMessage(),
$throwable->getFile(),
$throwable->getLine(),
$throwable->getTraceAsString()
));
// 2. 根据环境决定响应
if (getenv('APP_ENV') === 'production') {
echo "服务发生不可预期的错误,请稍后再试。";
// 发送 500 状态码
http_response_code(500);
} else {
// 开发环境显示详细错误(不推荐在生产使用)
echo "<pre>" . $throwable . "</pre>";
}
});
// 这会触发一个 "TypeError"(属于 Error 家族)
function add(int $a, int $b): int {
return $a + $b;
}
try {
add('hello', 10);
} catch (TypeError $e) {
// 主动捕获 TypeError:这次我们选择优雅处理
echo "类型错误:需要整数参数";
} catch (Throwable $e) {
// 兜底:捕获所有其他 Throwable(包括其他 Error 和 Exception)
echo "其他错误:". $e->getMessage();
}
✅ 好的做法:捕获特定 Exception 处理业务逻辑
<?php
class DatabaseConnectionException extends Exception {}
function connectDatabase(): PDO {
throw new DatabaseConnectionException("数据库连接超时");
}
try {
$pdo = connectDatabase();
} catch (DatabaseConnectionException $e) {
// 具体处理:重试或切换备用数据库
echo "数据库有问题,尝试备用...";
// $pdo = connectBackupDatabase();
} catch (Exception $e) {
// 捕获其他所有 Exception(如 PDOException)
echo "数据库其他错误: " . $e->getMessage();
}
❌ 糟糕的做法:滥用 catch (Throwable $e) 到处捕获
<?php
// 反面教材:在业务逻辑层到处 catch Throwable
function processUserData(array $data): void {
try {
// 这行代码如果 user_id 不存在,会触发 ValueError (PHP 8+)
$id = intval($data['user_id']);
// 这里如果 user 类不存在,会触发 Error
$user = new User();
} catch (Throwable $e) {
// 吞掉所有的错误!
// User 类真的不存在,程序应该崩溃报警,而不是静默失败
echo "处理失败";
}
}
为什么不好? 如果你在业务代码中捕获 Throwable,会掩盖代码中的 真正 bug(如:调用不存在的类、类型错误、除以零),这些 bug 应该通过崩溃暴露出来,迫使开发者修复,而不是被静默吞掉。
实战建议汇总
| 层级 | 应该捕获 | 不应该捕获 | 理由 |
|---|---|---|---|
| 全局入口(index.php) | Throwable |
无 | 防止任何崩溃暴露给用户,统一记录日志 |
| 框架/核心层 | Throwable 或 Error |
具体的 Error 子类 |
优雅降级(如:安全地终止请求) |
| 业务逻辑层(Service) | Exception 及其子类 |
Error 及其子类 |
处理预期的业务异常,让编程错误自然崩溃 |
| API/Controller 层 | 具体的 Exception 子类 |
Throwable |
返回合适的 HTTP 状态码和错误消息 |
PHP 8 的进步与注意点
ValueError(PHP 8.0):当传递的值正确但不在有效范围内时抛出(如array_rand([1,2], 0)),它属于Error,意味着这种问题通常是不可恢复的编程错误。TypeError变得更严格:PHP 8 对类型错误更严格,这属于Error,应该通过修复代码解决,而不是catch掉。
Throwable:统一了Exception和Error的顶层接口。Exception:处理业务逻辑异常,你应该主动try...catch它们。Error:处理引擎系统错误,你应该避免在业务代码中catch它们,除非是在全局兜底处。- 黄金法则:只捕获你预期会发生且你能合理处理的错误,其余让引擎崩溃,然后由全局处理器兜底记录。