从事 PHP 开发多年,最令人头疼的往往不是复杂的业务逻辑,而是异常处理代码如同“摸奖”——该捕获的异常漏掉了,该记录的日志缺失了,一旦线上出现问题,排查时毫无头绪。本文将深入剖析 PHP 异常捕获中的常见陷阱与必须遵守的规范,帮助开发者写出更健壮的代码。

先澄清一个非常普遍的误区:很多人误以为 try-catch 能够捕获所有运行时错误。实际上,PHP 的异常体系仅对 Exception 及其子类 生效。而 Warning、Notice 甚至 Fatal Error 这类错误(属于 Error 类体系)虽然与 Exception 都实现了 Throwable 接口,但处理逻辑截然不同——catch 块根本不会处理它们。
必须分清:哪些能 catch,哪些不能
能被 try-catch 捕获的,只有显式抛出的异常对象:
- 手动
throw new Exception("...")或自定义异常类实例 - 系统级异常:比如
PDOException(数据库操作失败)、InvalidArgumentException(参数非法),以及 PHP 7.3+ 启用JSON_THROW_ON_ERROR时抛出的JsonException
以下情形,try-catch 完全无能为力:
file_get_contents("missing.txt")—— 触发的是E_WARNING,并非异常,catch 无法接收- 调用未定义函数
foo()—— 直接引发Fatal error: Uncaught Error,脚本立即终止 - 语法错误、内存耗尽、类未找到(
Class not found)—— 统统属于 Fatal Error,catch 块根本不会执行
catch 块顺序与类型必须严谨
多个 catch 的排列顺序有一条硬性规则:子类在前,父类在后。一旦将父类放在前面,后面的子类 catch 就永远没有机会执行:
- ✅ 正确:
catch (PDOException $e)→catch (RuntimeException $e)→catch (Exception $e) - ❌ 错误:把
Exception放在最前面,后面所有子类 catch 全部失效,等于白写 - PHP 8+ 还支持单个 catch 捕获多类型:
catch (PDOException | RuntimeException $e),适合处理逻辑相同的场景,避免重复代码
finally 不是可选项,而是资源安全底线
finally 块的神奇之处在于:无论是否抛异常、是否被捕获,它都会执行。这是释放资源不可替代的机制:
- 文件句柄、数据库连接、临时锁、缓存标记……这些必须在
finally中显式关闭或清理 - 别只依赖
catch来处理——如果没异常,catch不执行,资源就会泄漏,后果严重 - 特别注意:
finally中的return会覆盖try或catch中的返回值,除非刻意为之,否则慎用
空 catch 是危险信号,日志是最低要求
写 catch (Exception $e) { } 相当于主动把问题埋进土里——线上出了错连个响都听不到。至少要做到:
- 记录完整上下文:
error_log($e->getTraceAsString(), 3, '/var/log/php/app.log'); - 生产环境禁止
echo $e->getMessage()或直接输出堆栈,防止敏感路径/配置泄露 - 不要用异常做流程控制:比如用
throw代替if,这会让逻辑变得难以追踪,得不偿失
