CI4全局异常处理必须通过app/Config/Exceptions.php的$handles配置,因框架启动时会覆盖set_exception_handler()且核心异常仅由Exceptions服务捕获;PageNotFoundException等需按命名空间精确配置处理器。

CI4 的全局异常处理不能靠改 index.php 或重写 _exception_handler 实现,必须走 app/Config/Exceptions.php 的 $handles 配置,否则自定义逻辑不会被框架调用。
为什么直接在 index.php 里 set_exception_handler() 失效
CI4 启动时会主动调用 service('exceptions')->initialize(),覆盖你手动设的处理器;即使没被覆盖,框架后续的异常捕获链(比如路由失败、模型验证)也只认它自己的 Exceptions 服务实例。你设的全局 handler 只能捕获最外层未被框架接管的异常,比如你在 Boot.php 之外抛出的 new Exception(),对 PageNotFoundException 或 DatabaseException 完全无效。
- 框架所有核心异常都实现了
ExceptionInterface,$handles数组正是按这个接口匹配的 - 手动设的
set_exception_handler不参与日志记录、视图渲染、HTTP 状态码设置等框架流程 - 开发环境里你看到的彩色错误页,就是
Exceptions服务渲染的,不是 PHP 原生机制
如何正确注册 PageNotFoundException 自定义处理
404 是最常要接管的异常,但不能只靠 set404Override() —— 它只管路由层未匹配,不管控制器方法不存在或中间件抛出的 PageNotFoundException。真正统一拦截得靠 Exceptions.php:
- 在
app/Config/Exceptions.php的$handles数组里加一项:\CodeIgniter\Router\PageNotFoundException::class => 'App\Exceptions\ErrorHandler::handle404' -
App\Exceptions\ErrorHandler必须是真实存在的类,handle404()方法需接收Throwable $e参数并返回Response实例 - 方法内必须显式调用
http_response_code(404)或$this->response->setStatusCode(404),否则浏览器收到的是 200 - 别在该方法里用
redirect(),那会返回 302,破坏 404 的语义;如需跳转,应由前端或 Nginx 层处理
DatabaseException 和 ModelException 怎么单独捕获
数据库操作失败默认显示白屏或堆栈,尤其在 production 下静默失败。你可以把它们从通用异常流中剥离开:
- 在
$handles中添加:\CodeIgniter\Database\Exceptions\DatabaseException::class => 'App\Exceptions\ErrorHandler::handleDbError' -
handleDbError()里可检查$e->getCode()区分是连接失败(1045)、超时(2006)还是语法错误(1064),再决定是重试、降级还是返回友好提示 - 注意:不要在处理函数里直接
die()或exit(),这会跳过响应发送,导致客户端卡住 - 如果用 RestServer,建议搭配
rest_handle_exceptions = true,它会把DatabaseException转成标准 JSON 错误响应,状态码自动设为HTTP_INTERNAL_ERROR(500)
生产环境里日志和告警怎么联动
光有自定义页面不够,关键错误得进监控。CI4 的 Exceptions 服务默认用 LogWriter,但不支持推送:
- 在自定义 handler 方法里,调用
log_message('error', $e->getMessage(), ['trace' => $e->getTraceAsString()])写入文件 - 若用 Redis 做告警队列,可在 handler 末尾加
redis_list_add($e->__toString())(参考 CI3 的实现方式,注意Redis连接要复用) - 避免在 handler 里做耗时操作(如远程 HTTP 请求),否则拖慢整个错误响应,甚至触发超时
- 特别注意
PageNotFoundException在高流量站点可能每秒几百次,别让它刷爆日志——加个计数器或采样率控制
最易被忽略的是:$handles 数组的类名必须带完整命名空间,且类文件路径要符合 PSR-4;写错一个字母,框架就静默 fallback 到默认处理,连错误日志都不会记——因为连匹配都没匹配上。











