PHP 8.6:finally 里写 return 会丢掉异常,编译期就报弃用
PHP 8.6 的弃用清单里有一条:在 finally 块中使用 return。它会触发编译期的 deprecation,而不是运行到那一行时才报。来源见 PHP 8.6 Deprecated Features 中的 “Returning from a finally block” 一节,对应的 RFC 是 deprecations_php_8_6。
PHP 8.6 计划 2026-11-19 GA,该条已通过,级别为编译期 deprecation;到 PHP 9.0 会升级为错误。
<?php
function getConfig(): array
{
try {
return loadConfig(); // throws if the file is missing
} finally {
return []; // Deprecated: the exception is gone, the caller just gets []
}
}
报在编译期,语法检查阶段就能暴露
这一点比危害本身更值得注意:它不是等到某次请求真的走进 finally 才触发,而是文件被编译时就会报。也就是说 php -l 这类语法检查、CI 里的 lint 步骤就能扫出来,不需要构造出会抛异常的场景。上线前拦得住,而不是等线上出问题再回头翻。
真正的问题:异常消失了
finally 的语义是无论 try / catch 如何结束都要执行。一旦在 finally 里 return,整个函数就此结束:
try里抛出的异常不再向上传播,异常对象直接被丢掉;try里的return值被丢弃;catch里处理完的返回值、乃至catch中尚未处理的异常,同样被覆盖。
上面那个例子,loadConfig() 因为文件缺失抛了异常,调用方却拿到一个空数组 []。线上表现是「配置读不到,但没有任何报错」——排查时看不到堆栈,日志里也没有痕迹。8.6 之前这种行为一直合法,什么都不会报,属于 PHP 里存在很久的坑。
正确写法
同一个结果基本都能不带 finally 里的 return 表达出来。最直接的做法是把 return 移到 try / catch 之后,finally 只留资源清理:
<?php
function getConfig(): array
{
try {
$config = loadConfig();
} catch (ConfigException $e) {
$config = [];
} finally {
// 只做清理,比如关闭句柄、释放锁
}
return $config;
}
如果确实要吞掉异常并返回兜底值,就显式 catch 并记日志,让失败至少留下痕迹:
<?php
function getConfig(): array
{
try {
return loadConfig();
} catch (Throwable $e) {
error_log('loadConfig failed: ' . $e->getMessage());
return [];
}
}
怎么排查
grep -rn --include='*.php' 'finally' .,逐个看块内有没有return;php -l file.php,PHP 8.6 下会直接给出 deprecation;- 升级到支持 8.6 的 PHPStan / Psalm 后跑一遍扫描,静态分析也能定位到这类写法。
改动本身很小:把 return 挪出 finally,或者改成显式 catch。趁现在还是 deprecation,批量处理成本最低;等到 PHP 9.0 变成 error,就是编译不过的问题了。