Menu

PHP 异常:throw 与自定义异常类

在 PHP 中用 throw new Exception('message'); 抛出异常,调用方在 catch 块中用 $e->getMessage() 读取信息。内容包括内置异常类、编写自定义异常、getCode()、getLine() 和 getPrevious()、作为表达式的 throw,以及 PHP 自身抛出的 ValueError、TypeError 等错误。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

在 PHP 中用 throw new Exception('message'); 抛出异常。程序在这一行停止,跳转到最近的、能接受该异常类型的 catch 块,在那里 $e->getMessage() 返回异常信息。使用 InvalidArgumentException 这样的具体类,调用方就能区分不同的问题。

异常是一个对象。throw 把它沿调用栈向上传递,直到第一个有匹配 catch 的调用方,由它决定怎么处理;发现问题的函数不需要知道这些。捕获的机制、finally 以及捕获多种类型,见 try catch 页面。

异常对象包含什么

每个异常都带有一条信息、一个整数代码、创建它的文件和行号,以及调用栈跟踪。构造函数接受 ($message, $code, $previous),都是可选的:

getLine() 是 throw new 所在的行,而不是 catch 所在的行。跟踪信息列出了走到这里所经过的调用(#0 /home/index.php(8): findOrder()),日志中出现错误时,通常首先要看的就是它。把异常转换成字符串(echo $e; 或 (string) $e)会以 PHP 的标准格式输出全部信息。

内置异常类

PHP 在标准 PHP 库(SPL)中提供了一系列异常类。抛出能说明问题的那个类,代码更易读,调用方也能精确捕获。下面这些类都继承自 Exception:

类什么时候抛出
InvalidArgumentException参数形式不对:名称为空、未知的选项
DomainException值超出了合理的范围:价格为负数
OutOfRangeException代码请求了一个永远不可能存在的索引(调用方的 bug)
OutOfBoundsException只有运行时才知道的数据中缺少某个键或索引
RangeException运行时计算出的结果超出了有效范围
LengthException某个东西太长或太短
RuntimeException只有运行时才能发现的问题:磁盘已满、超时
UnexpectedValueException函数返回或接收到了类型出乎意料的值
LogicException代码本身有误,是 bug 而不是输入有问题
JsonException使用 JSON_THROW_ON_ERROR 时由 json_encode()/json_decode() 抛出

InvalidArgumentException、DomainException、LengthException 和 OutOfRangeException 继承自 LogicException;OutOfBoundsException、RangeException 和 UnexpectedValueException 继承自 RuntimeException。你可以自己查看任何类的父类:

TypeError 的父类是 Error,不是 Exception:它属于下面要讲的第二个家族。

PHP 自身抛出的错误

从 PHP 7 起,尤其是 PHP 8 以后,PHP 自己的函数和运算符遇到以前只是警告的问题时,会抛出 Error 的子类。捕获方式相同,但 catch (Exception $e) 匹配不到它们:

DivisionByZeroError、TypeError、ValueError、ArgumentCountError、UnhandledMatchError 以及普通的 Error 都继承自 Error。它们往往说明调用代码有 bug,而不是数据有问题,所以被放在 Exception 体系之外。正是 declare(strict_types=1) 让 str_repeat(5, 2) 产生 TypeError;没有它,PHP 会把 5 转换成 "5"。

编写自定义异常类

通常一行子类就够了:类名本身就表达了含义,调用方可以精确捕获这种类型。捕获方需要数据(哪个订单、多少金额)时,就添加属性,并用 parent::__construct() 把信息传给父类:

选父类时要谨慎:继承 RuntimeException(而不是普通的 Exception)意味着捕获 RuntimeException 的代码也会处理你的异常。建立一个小的层级结构,每个库或模块一个基础异常,下面再分具体的子类,调用方就可以在“捕获支付模块的所有异常”和“只捕获这一种情况”之间选择。

用 previous 串联异常

捕获一个底层异常、再抛出一个更高层的异常时,把原来的异常作为第三个参数传入。这样什么都不会丢失:getPrevious() 可以一路回溯到根本原因。

用户看到的是“customers.csv could not be imported”;日志顺着异常链,还能拿到“row 2 has 2 columns”。如果串联的异常一直没有被捕获,PHP 的致命错误会输出整条链:先是最初的原因,然后是每一层包装,各自位于一行 Next 之下。

作为表达式的 throw

从 PHP 8.0 起,throw 是一个表达式,所以可以出现在任何需要值的地方:?? 之后、三元运算中、箭头函数里。“取到这个值,否则失败”就能写成一行:

什么时候不该抛出异常

异常用于当前函数无法处理、正常流程也预料不到的情况。搜索没有结果、可选字段为空、用户没有登录,这些都是普通的结果:返回 null、false 或空数组,让调用方去检查;只有在继续运行会出错的情况下才使用异常。

还有两个习惯能让异常代码保持清晰:不要为了忽略异常而捕获它(空的 catch (Exception $e) {} 会把下一个真正的 bug 藏起来);抛出具体的类,而不是光秃秃的 Exception,这样调用方就不必为了处理一种情况而捕获所有异常。

常见问题

PHP 中怎么抛出异常?

创建一个异常对象并抛出:throw new InvalidArgumentException('Quantity must be positive');。程序在这一行停止,跳转到最近的匹配 catch 块;如果没有匹配的块,脚本会以“Uncaught”致命错误结束。

PHP 中怎么创建自定义异常?

继承 Exception 或它的某个子类:class PaymentFailedException extends RuntimeException {}。只要这一行,就可以按它自己的类型抛出和捕获。捕获方需要额外数据时,再添加属性和构造函数,并调用 parent::__construct($message, $code, $previous)。

getMessage 和 getCode 有什么区别?

getMessage() 返回作为构造函数第一个参数传入的文本,供人阅读和写入日志。getCode() 返回作为第二个参数传入的整数(默认为 0),适合程序判断,比如把错误映射为 HTTP 状态码。

PHP 中应该抛出哪种异常?

选用与问题相符的 SPL 内置类:参数有误用 InvalidArgumentException,值超出允许范围用 DomainException,只有运行时才能发现的故障(磁盘已满、超时)用 RuntimeException,编程错误用 LogicException。调用方需要区分的错误,就创建你自己的子类。

PHP 中的异常链是什么?

在抛出新异常时,把原来的异常作为构造函数的第三个参数传入:throw new ImportException('Import failed', 0, $e);。新异常提供上下文,对它调用 $e->getPrevious() 会返回最初的原因,所以不会丢失任何细节。

Coddy 编程语言插图

和 Coddy 一起学编程

开始