Fatal error: Uncaught TypeError 之后,我养成了「三秒看栈、十秒定位」的肌肉记忆
写插件最烦的不是报错本身,是报错信息把你往反方向带。上周有个兄弟在群里甩了张图:Fatal error: Uncaught TypeError: Argument 1 passed to MyPlugin\Tracker::record() must be of the type array, null given,下面跟了三十多行栈追踪。他盯着 record() 方法看了二十分钟,其实真凶在八层调用之外。
这篇不罗列所有 Exception 类型,只聊我实战中攒下的「报错→定位」条件反射,按报错形态分三类。
一、TypeError / ArgumentCountError:别在案发地点找凶手
PHP 7+ 的严格类型让这类报错爆炸式增长。新手常犯的错误:顺着栈顶往下读,把最后一帧当犯罪现场。
实际该看的顺序:
- 先看第一帧(栈顶):确认哪个函数被调崩了、期望什么类型
- 跳最后一帧(栈底):谁最先发起的调用?传入的变量从哪来?
- 中间帧挑自己代码的边界看:插件钩子、filter 回调、Ajax 入口这些"接口层"
举个真事。我的定时任务回调大概长这样:
add_action( 'myplugin_daily_sync', [ $this->syncer, 'run' ] );
// Syncer 类
public function run( array $config ) {
$this->tracker->record( $config );
}
报错说 record() 要 array 给了 null。但 run() 的 $config 哪来的?WP Cron 的 action 调度是 do_action( 'myplugin_daily_sync' ),不带参数。所以 $config 是 null,一路传到底才炸。
定位口诀:TypeError 找源头,ArgumentCountError 找调度。WP 的钩子机制让参数传递变成"暗渠",栈追踪里看不到 do_action 传了啥,得去 wp_schedule_event 的注册点核对参数签名。
二、RuntimeException / 自定义异常:被吞掉的「中间层」
有些插件框架爱包 try-catch,把异常转成 WP_Error 或者干脆记个日志继续跑。结果页面白屏没报错,数据却不对,你以为是逻辑 bug,其实是异常被吃了。
我现在的习惯:在开发环境给 wp-content/debug.log 加一道「异常哨兵」:
set_exception_handler( function( $e ) {
if ( $e instanceof \RuntimeException ) {
error_log( '[RUNTIME] ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString() );
// 开发环境直接抛,别吞
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
throw $e;
}
}
} );
重点盯两类自定义异常:
- HTTP 层封装异常:比如把 Guzzle 的
ConnectException包成MyPlugin\ApiException,但丢了getHandlerContext()里的 DNS/SSL 细节 - 数据库事务回滚异常:
$wpdb->query( 'ROLLBACK' )自己失败不会抛,但你的业务异常已经被 catch 过了,两头空
定位技巧:在 catch 块里强制 throw $e 重跑一遍,看原始异常链。PHP 8 的 $e->getPrevious() 一定要链式追到底。
三、Error(非 Exception):编译期陷阱与隐式加载
这类最阴,因为 try-catch( Exception $e ) 抓不住 Error,得用 catch( Throwable $t )。
高频踩坑场景:
| 报错 | 表面位置 | 实际雷区 |
|---|---|---|
Class 'MyPlugin\Vendor\SomeLib' not found | new 语句 | composer autoload 没更新,或 PSR-4 路径大小写和类名对不上(Linux 生产机才炸) |
Cannot declare class MyPlugin\Admin, because the name is already in use | require_once | 同文件被不同路径引用(软链接、大小写别名),_once 的哈希判断失效 |
Allowed memory size exhausted | 任意位置 | WP_Query 没加 'no_found_rows' => true,或者循环里重复 new WP_Query 没 reset |
最后这个内存溢出,我有一次追了两个小时,发现是 register_activation_hook 里批量插入测试数据,用了 wp_insert_post() 但每插一条都触发 save_post,我的回调又调了 wp_update_post(),死循环到内存炸。栈追踪最后一帧在 wpdb->insert(),完全无关。
我的「三秒十秒」流程
现在看到报错,手比脑子快:
- 0-3 秒:扫报错类名(TypeError/Error/RuntimeException?),扫文件路径(核心/主题/插件/ vendor?),判断「这是谁的锅」
- 3-10 秒:TypeError 看栈底调用源;Error 看 autoload/文件系统;RuntimeException 看有没有被包 try-catch
- 10 秒后:还没头绪,直接
grep -r "那个函数名" --include="*.php"全项目搜调用点,往往比读栈快
有个细节:WP 的 wp_die() 在 Ajax 请求里会套 HTML,把栈追踪埋进注释。按 F12 看响应原文,比盯着「0」状态码瞎猜强十倍。
你们有没有被报错信息「指东打西」坑过的经历?或者哪种 Exception 的定位套路特别反直觉?

