Exception 与 Error 的继承关系图
解读
国内一线、二线互联网公司的 PHP 面试里,这道题出现的频率极高。面试官真正想验证的,并不是候选人能否把一张“图”背下来,而是能否用“嘴”把 Throwable 体系讲清楚:
- PHP7 之后到底把“可抛”分成了哪两大支?
- 哪一支能被 try/catch,哪一支会触发致命中断?
- 自定义业务异常到底该继承谁?
- 注册全局异常/错误处理器时,类型约束该怎么写?
把继承链说顺了,才能证明你对“异常安全”“错误降级”“统一日志”这些生产级方案有体感,而不是只会写 echo。
知识点
- PHP7 统一了“异常”与“错误”的顶层接口:Throwable
- 位置:内核 zend_exceptions.h,C 层定义
- 性质:interface,不能被直接 new,只能被实现或继承
- 两大抽象分支
- Exception(用户级、业务级、可恢复)
- Error(引擎级、语法级、不可恢复)
- 常见子类
- Exception → LogicException、RuntimeException → 自定义业务异常
- Error → TypeError、ParseError、AssertionError、DivisionByZeroError 等
- 捕获规则
- catch (Throwable $e) 可一网打尽
- catch (Error $e) 会漏掉 Exception
- catch (Exception $e) 会漏掉 Error
- 设计意义
- 让框架可以 set_exception_handler 统一兜底
- 让业务层只关心 Exception,避免把语法错误当业务异常处理
- 版本差异
- PHP5 时代只有 Exception,Fatal Error 不可捕获
- PHP7+ 所有 Fatal/Recoverable 全部转成 Error 类,可 catch
答案
“PHP7 开始,所有可以 throw 的东西都必须实现 Throwable 接口。
Throwable 向下分成两条主线:
第一条是 Exception 抽象类,所有业务异常、框架异常、自定义异常都继承它,例如 LogicException、RuntimeException、我们项目里的 BizException。
第二条是 Error 抽象类,代表引擎自身问题,比如传参类型不对会抛 TypeError,文件语法写错会抛 ParseError,调用未定义函数会抛 Error,这些过去是 Fatal,现在可捕获但通常不建议恢复。
因此,继承关系用一句话概括:
Throwable →(Exception ← 业务异常)
Throwable →(Error ← 引擎错误)
画成嘴里的‘图’就是一棵深度为 2 的树,根节点是 Throwable,第二层左侧是 Exception,右侧是 Error,再往下才是各个具体子类。
实际编码时,如果想兜底所有抛出的东西,就 catch (Throwable e);千万别再写 catch (Exception $e) 去捕获语法错误,那是 PHP5 的惯性思维。”
拓展思考
- 生产级日志方案
在 set_exception_handler 里统一记录 Throwable,而不是只记 Exception,才能拿到 ParseError、TypeError 这类“上线后才发现”的致命信息。 - 错误码与 HTTP 状态映射
自定义 BizException 可带 $code,在异常监听器里把 4xx 业务异常和 5xx 系统错误分开打点和告警,避免半夜被“余额不足”叫醒。 - 向下兼容陷阱
老库如果抛出旧式类,实现了 Throwable 但继承自 Exception,会导致 catch (Error $e) 捕获不到;升级 SDK 时要回归测试所有 catch 块。 - 与 Swoole/FPM 差异
Swoole 协程里一旦捕获到 Error,建议直接让 Worker 退出,由 Manager 拉新进程,防止扩展内部状态污染;FPM 模式则可放心恢复。 - 面试反向提问
当面试官问完继承图,你可以追问:“贵司对 Error 的监控策略是怎样的?是否用 Throwable 统一上报到 Sentry/Zipkin?” 既展示深度,又体现工程化思维。