PHP错误与异常必须分开处理,因set_exception_handler()仅捕获Exception和Error类(如ParseError),不处理E_WARNING等传统错误,且E_PARSE发生在编译期无法被捕获。

PHP 的错误和异常必须分开处理,混用会导致 E_ERROR、E_PARSE 漏捕获,set_exception_handler() 对传统错误完全无效。
为什么 set_exception_handler() 抓不到 ParseError 或 Warning
set_exception_handler() 只接管未被捕获的 Exception 和 PHP 7+ 的 Error 类(如 TypeError、ParseError),但不处理 E_WARNING、E_NOTICE 这类传统错误。而 E_PARSE 发生在编译阶段,脚本根本没执行到注册函数那一步;E_WARNING 默认只是打个屏、返回 false,不中断也不抛异常。
常见错误现象:
- 线上页面突然白屏,日志里却没记录 —— 很可能是
E_PARSE或E_ERROR没被register_shutdown_function()捕获 -
file_get_contents()失败只返回 false,但错误信息直接输出到 HTML 里 —— 因为它触发的是E_WARNING,不是异常
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
register_shutdown_function()+error_get_last()补全致命错误兜底 - 对
E_WARNING等可恢复错误,用set_error_handler()转成ErrorException再 throw,才能进try/catch或全局异常处理器 - 别依赖
@抑制警告,它只是隐藏输出,不改变错误发生事实,且会拖慢性能
怎么让 E_WARNING 也能被 try/catch 捕获
核心是手动把错误“升格”为异常:在 set_error_handler() 回调里,对需要拦截的错误级别(如 E_WARNING)实例化并抛出 ErrorException。
注意构造参数顺序:new ErrorException($message, 0, $severity, $file, $line) —— 第二个参数是 $code(通常填 0),第三个才是 $severity(即原始错误级别)。填反会导致 $e->getCode() 返回错乱值。
示例片段:
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
$errorLevels = [E_WARNING, E_USER_WARNING];
if (in_array($errno, $errorLevels)) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
return false; // 不处理的错误交还给默认机制
});
关键点:
- 返回
false表示不接管,避免覆盖你不想动的E_NOTICE - 生产环境慎转
E_NOTICE,它数量大、价值低,全转异常可能压垮 try/catch 链路 - 该 handler 必须在所有业务逻辑前注册,否则中间抛出的错误已漏掉
自定义异常类该怎么写才不白费功夫
不推荐只写一个泛用的 AppException。真正有用的自定义异常要能区分场景、携带上下文、支撑后续监控和降级策略。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 按业务域分,比如
DatabaseException、ValidationException、PaymentException;别按严重程度分(如FatalException),语义模糊且无法多继承 - 构造函数里传
$code建议用整数,方便映射 HTTP 状态码(如422给验证失败,503给服务不可用) - 加业务字段(如
private $userId、private $orderId)和对应 getter,日志里就能提取关键线索,不用翻堆栈 - 别重写
__toString()或实现额外接口,PHP 自带的getMessage()、getTraceAsString()已够用
一个最小可用的 ValidationException:
class ValidationException extends Exception
{
private $fields;
public function __construct($message = '', int $code = 422, array $fields = [], Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->fields = $fields;
}
public function getFields(): array
{
return $this->fields;
}
}
日志写入时最容易丢数据的三个地方
错误日志看似简单,但多进程并发、权限不足、路径不存在这三件事,能让 error_log() 或 file_put_contents() 静默失败。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用
file_put_contents($logPath, $content, FILE_APPEND | LOCK_EX),LOCK_EX防止多请求写花日志 - 写日志前先
is_writable(dirname($logPath))检查目录权限,失败时 fallback 到syslog()或直接trigger_error() - 日志路径别写相对路径,用
__DIR__ . '/logs/error.log'明确位置,避免因工作目录变化导致写入失败 - 别在异常处理器里再抛异常或调用可能失败的函数(如
mail()),否则二次崩溃,连日志都留不下
最常被忽略的是:register_shutdown_function() 捕获的致命错误,其上下文(如 $_SERVER、$_POST)可能已被销毁或截断,取之前先 isset() 判断,别直接 json_encode($_SERVER) 硬塞。



















