线上项目运行得好好的,用户突然反馈页面打不开,只看到一串英文报错。致命错误(Fatal Error)一旦直接输出到浏览器,不仅会破坏页面布局,还可能泄露文件绝对路径、框架目录甚至数据库账号等信息。隐藏致命错误的核心原则是:面向用户关闭显示,面向开发者保留日志。

在 php.ini 中,真正影响错误展示的不是单独一个开关,而是多个指令叠加。我们可以把渲染给用户的错误输出和记录给开发者的错误日志拆开处理,这样既能防扰,也不会让问题偷偷消失。
一、先把三层错误开关配置清楚
PHP 的错误展示主要由 display_errors、display_startup_errors 和 error_reporting 共同决定。display_errors 控制运行时错误是否输出到页面,error_reporting 决定哪些级别需要被报告。如果只把 display_errors 改成 Off,但 error_reporting 仍然为 E_ALL,错误不会显示,可日志里依然能记录;反过来,如果把 error_reporting 设为 0,那么连日志都收不到,这是很多隐藏报错后无法排错的原因。
开发环境建议打开全部显示,生产环境则关闭页面输出、开启日志写入。可以在 php.ini 里写死,也可以在入口文件用 ini_set 动态调整。下面是一个典型示例:
<?php
// 开发环境:页面显示所有错误
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
// 生产环境:关闭显示,写入日志
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php_errors.log');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
?>
这里 log_errors 建议保持开启,error_log 指向一个 Web 不可访问的目录,避免日志文件被下载。对于 E_DEPRECATED 和 E_NOTICE,生产环境可以选择屏蔽,但 E_ERROR、E_WARNING、E_PARSE 等关键级别不建议从 error_reporting 中移除。
二、用 set_error_handler 接管常规错误
PHP 的 set_error_handler 可以拦截常规错误,比如 Warning、Notice、Deprecated。它最大的意义是让我们统一错误输出格式,或者在错误发生时抛出异常,让业务层用 try catch 处理。需要注意的是,它无法捕获 E_ERROR、E_PARSE、E_CORE_ERROR 等致命级错误,所以只能作为第一层防线。
下面这段代码把常规错误转换成 ErrorException,这样原本散落各处的 Warning 就能进入异常处理流程。回调中先判断 error_reporting() 与当前错误级别的交集,是为了尊重 @ 抑制符和当前错误级别设置。
<?php
set_error_handler(function ($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
return false;
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
try {
$content = file_get_contents('/tmp/maybe_missing.txt');
} catch (Throwable $e) {
error_log($e->getMessage());
echo '文件读取失败,请稍后再试';
}
?>
在捕获到异常后,页面只给用户一句可读提示,真实错误通过 error_log 写入日志。这样做比直接用 die 打印 error 更安全。需要注意的是,如果回调里不返回 false,错误会被吞掉,脚本继续执行;返回 false 则交回 PHP 默认处理器,这在需要保持原生行为时有用。
三、用 register_shutdown_function 兜底致命错误
致命错误发生时,PHP 不会调用 set_error_handler,但脚本结束前仍会执行 register_shutdown_function 注册的回调。我们可以在回调里通过 error_get_last 获取最后一个错误,判断它是否属于致命级别,然后输出统一错误页或返回 JSON 提示。
下面是一个常见的兜底方案,适合在接口入口或全局配置文件中注册:
<?php
register_shutdown_function(function () {
$error = error_get_last();
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
if ($error !== null && in_array($error['type'], $fatalTypes, true)) {
error_log($error['message'] . ' in ' . $error['file'] . ':' . $error['line']);
http_response_code(500);
exit('系统暂时不可用,请稍后重试');
}
});
?>
这个回调并不能恢复脚本执行,它只能做收尾工作,比如记录日志、返回友好状态码。对于一些解析错误,如果错误发生在注册回调的文件之前,脚本可能完全无法执行,这时需要在 php.ini 的 auto_prepend_file 中引入一个提前注册 shutdown 函数的文件,才能覆盖更多情况。
另外,匿名函数中调用 exit 会立即结束脚本,后续的 shutdown 函数不会再执行。如果页面还有多个 shutdown 回调,建议把输出逻辑拆出去,或避免在回调里直接 exit。
四、生产环境防扰的完整策略与常见误区
把 display_errors 设为 Off 只是第一步。生产环境还需要设置统一的 500 错误页面,避免服务器默认错误页出现。Nginx 或 Apache 可以通过 error_page 指令指向静态页面,PHP 应用则可以在最外层捕获异常后渲染友好视图。日志方面,建议按天切割,配合监控告警,而不是让错误日志无限增长。
常见的误区是大量使用 @ 符号抑制错误。@ 虽然能让单条语句不输出错误,但它会降低性能,而且如果该语句产生致命错误,显示行为不会受 @ 控制,仍然可能暴露信息。另一个误区是遇到错误直接 die('error'),这样页面虽然不显示堆栈,但用户看到一条干巴巴的错误提示,体验依然很差,开发者也拿不到上下文。
更合理的做法是:页面输出只包含用户可理解的提示,错误细节全部走 log_errors 和 error_log。如果业务允许,可以在日志中记录请求方法、URL、用户 ID 等上下文,这样复现问题时不需要反复和用户沟通。隐藏致命错误不是掩盖问题,而是把错误信息从公开页面转移到内部日志系统。
PHP错误处理致命错误隐藏display_errors修改时间:2026-10-04 23:08:19