Exception 与 Error 的继承关系图

解读

国内一线、二线互联网公司的 PHP 面试里,这道题出现的频率极高。面试官真正想验证的,并不是候选人能否把一张“图”背下来,而是能否用“嘴”把 Throwable 体系讲清楚:

  1. PHP7 之后到底把“可抛”分成了哪两大支?
  2. 哪一支能被 try/catch,哪一支会触发致命中断?
  3. 自定义业务异常到底该继承谁?
  4. 注册全局异常/错误处理器时,类型约束该怎么写?

把继承链说顺了,才能证明你对“异常安全”“错误降级”“统一日志”这些生产级方案有体感,而不是只会写 echo。

知识点

  1. PHP7 统一了“异常”与“错误”的顶层接口:Throwable
    • 位置:内核 zend_exceptions.h,C 层定义
    • 性质:interface,不能被直接 new,只能被实现或继承
  2. 两大抽象分支
    • Exception(用户级、业务级、可恢复)
    • Error(引擎级、语法级、不可恢复)
  3. 常见子类
    • Exception → LogicException、RuntimeException → 自定义业务异常
    • Error → TypeError、ParseError、AssertionError、DivisionByZeroError 等
  4. 捕获规则
    • catch (Throwable $e) 可一网打尽
    • catch (Error $e) 会漏掉 Exception
    • catch (Exception $e) 会漏掉 Error
  5. 设计意义
    • 让框架可以 set_exception_handler 统一兜底
    • 让业务层只关心 Exception,避免把语法错误当业务异常处理
  6. 版本差异
    • 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(Exceptione);如果只关心业务,就 catch (Exception e);千万别再写 catch (Exception $e) 去捕获语法错误,那是 PHP5 的惯性思维。”

拓展思考

  1. 生产级日志方案
    在 set_exception_handler 里统一记录 Throwable,而不是只记 Exception,才能拿到 ParseError、TypeError 这类“上线后才发现”的致命信息。
  2. 错误码与 HTTP 状态映射
    自定义 BizException 可带 $code,在异常监听器里把 4xx 业务异常和 5xx 系统错误分开打点和告警,避免半夜被“余额不足”叫醒。
  3. 向下兼容陷阱
    老库如果抛出旧式类,实现了 Throwable 但继承自 Exception,会导致 catch (Error $e) 捕获不到;升级 SDK 时要回归测试所有 catch 块。
  4. 与 Swoole/FPM 差异
    Swoole 协程里一旦捕获到 Error,建议直接让 Worker 退出,由 Manager 拉新进程,防止扩展内部状态污染;FPM 模式则可放心恢复。
  5. 面试反向提问
    当面试官问完继承图,你可以追问:“贵司对 Error 的监控策略是怎样的?是否用 Throwable 统一上报到 Sentry/Zipkin?” 既展示深度,又体现工程化思维。