因为fatal error多发生在脚本加载或执行中断阶段(如语法错误、未定义函数、内存耗尽),此时php无法进入用户态执行set_exception_handler注册的回调,故无法捕获;仅运行时抛出的error子类(如typeerror)可被其捕获。

set_exception_handler 为什么没捕获到 Fatal Error
PHP 7+ 把大部分致命错误(如 ParseError、TypeError、FatalError)统一实现了 Throwable 接口,set_exception_handler() 理论上能捕获它们——但实际常失效,原因很具体:
-
parse error(语法错误)发生在脚本加载阶段,根本不会执行到set_exception_handler()注册逻辑 -
call to undefined function或class not found这类 fatal,在 autoloader 失败后直接中止,不触发 handler - 内存耗尽(
Allowed memory size exhausted)等底层错误,PHP 无法安全进入用户态 handler
真正能稳定捕获的,是运行时抛出的 Exception 和继承自 Throwable 的错误(如 TypeError 在函数调用中发生时)。
render() 返回值必须是 Response 实例,否则被框架二次包装
在 ThinkPHP 等框架里,自定义异常处理器的 render() 方法若返回字符串或数组,框架会自动把它塞进默认 HTML 模板里,导致 API 接口返回混杂的 HTML 内容和 JSON 数据。这不是 bug,是框架强约束:
- 必须显式调用
response()或json()并 return 它的返回值 - 不要在
render()里用echo、die或exit,这会绕过框架响应生命周期 - HTTP 状态码别依赖
$e->getStatusCode(),很多异常类型(如TypeError)根本不实现该方法,返回 0;应手动指定,比如json([...], 500)
report() 里 trace 太长会静默截断,敏感字段得主动过滤
日志写入 MySQL 时,$e->getTraceAsString() 动辄上千行,超出 TEXT 字段长度后会被截断,关键调用栈丢失。更危险的是,$_POST、$_SERVER['HTTP_AUTHORIZATION'] 等可能随 $e->getTrace() 一起被序列化进日志。
- 用
array_slice($e->getTrace(), 0, 10)控制堆栈深度 - 避免直接记录完整上下文:检查异常消息是否含
'SQLSTATE'或'password'等关键词,再决定是否脱敏 - 异步写日志更稳:
Log::channel('daily')->error(...)比同步file_put_contents更少阻塞主流程
自定义异常类在框架中被隐式转成 HttpException
ThinkPHP 会在异常被捕获前做一层封装:只要你的自定义异常类有 httpCode 属性或继承了 think\Exception,它就会被转成 HttpException 实例。结果是 render() 收到的永远是 HttpException,原始类型信息丢失。
解决办法只有两个:
- 在
render()里用$e->getPrevious()反向取原始异常(前提是抛出时用了异常链:throw new HttpException(500, 'msg', $original)) - 放弃继承框架异常基类,改用纯
Exception或RuntimeException,并在report()中打标识别:if ($e instanceof PaymentFailedException) { ... }
最易被忽略的点是:CLI 命令(如 php think queue:work)默认不加载 app.php 的 app_exception 配置,必须手动绑定处理器,否则连 render() 都不会进。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











