Hyperf 中子协程(co/go)抛出的未捕获异常默认不被全局 ExceptionHandler 捕获,会直接终止协程且不冒泡;必须在子协程内手动 try/catch 并记录日志,不可依赖外部处理器。
Hyperf 中子协程(如
、
、
)抛出的未捕获异常,**默认不会被全局
捕获**——它会直接终止该协程,且不冒泡到父协程或 HTTP 请求生命周期中。这是协程调度模型决定的,不是配置遗漏。
为什么子协程异常不走全局 ExceptionHandler
Hyperf 的全局异常处理器(如
)只接管「HTTP 请求主协程」中未被捕获的异常。而
或
启动的是独立协程,其异常由 Swoole 协程调度器直接处理,不经过 Hyperf 的请求上下文和异常分发链。
错误现象:子协程内
,日志里只有
,但
完全没执行
根本原因:子协程与 HTTP 请求协程是平行关系,不是父子继承关系;异常无法跨协程边界自动传播
影响:这类异常不会记录到
配置的日志处理器中,也不会返回标准错误响应,容易漏监控
必须在子协程内手动 try/catch
这是最直接、最可控的解法。不能依赖外部捕获,必须在协程启动的闭包内部兜底。
正确写法:每个
/
闭包第一层就加
,并显式记录或上报
别偷懒写成
—— 里面任何位置抛异常都会静默失败
建议统一用
记录,带上
方便追踪
示例:
避免用 set_exception_handler 替代
有人试图在子协程里调用
,这是无效的:
是 PHP 进程级的,对协程无感知,Swoole 协程中触发时不会进入该回调
Hyperf 的
机制本身也不作用于子协程,它是基于
绑定在请求生命周期上的
强行注册反而可能污染 Worker 进程的全局状态,导致后续请求异常处理错乱
进阶:封装安全协程启动工具
如果项目中大量使用子协程,重复写
易出错。可封装一个安全版启动函数:
注意:这个封装不能替代业务层的精准错误处理,仅作为兜底。真正需要重试、降级或用户提示的逻辑,仍得在
块内明确实现。
子协程异常不冒泡是协程模型的固有特性,不是 bug。指望全局处理器一劳永逸地覆盖所有协程,只会让问题更隐蔽——日志缺失、监控断点、线上故障难复现。最稳妥的做法,就是把每个
当作一个微型“请求”来对待:自己负责初始化、自己兜底异常、自己打标日志。
cogoCoroutine::createExceptionHandlerApp\Exception\Handler\AppExceptionHandlercogothrow new RuntimeException('xxx')PHP Fatal error: Uncaught RuntimeExceptionAppExceptionHandler::handleexceptions.phpcogotry/catchco(function () { /* 大段逻辑 */ });Hyperf\Logger\LoggerFactorycoroutine_id()use Hyperf\Logger\LoggerFactory;
use Hyperf\Coroutine\Coroutine;
$logger = $this->container->get(LoggerFactory::class)->get('job');
Hyperf 3.1.66
本页面提供企业级 PHP 协程框架 Hyperf 3.1.66 版本的官方源码下载与完整更新日志。重点解析 v3.1.66 版本中新增的 gRPC 多客户端负载均衡支持、Pool 连接池全量刷新、Guzzle 持久化 Cookie 以及数据库 JSON 包含键查询等核心优化特性。
下载
co(function () use ($logger) {
try {
// 业务逻辑,可能 throw
$result = $this->heavyTask();
$logger->info('sub-coroutine success', ['cid' => coroutine_id()]);
} catch (\Throwable $e) {
// 必须在这里处理,否则异常丢失
$logger->error('sub-coroutine failed', [
'cid' => coroutine_id(),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
// 可选:上报 Sentry / Prometheus 等
}
});
set_exception_handlerset_exception_handlerExceptionHandlerHyperf\HttpServer\Middleware\ErrorMiddlewaretry/catchfunction safe_co(callable $callable, string $context = 'unknown'): void
{
co(function () use ($callable, $context) {
try {
$callable();
} catch (\Throwable $e) {
$logger = \Hyperf\Utils\ApplicationContext::getContainer()
->get(\Hyperf\Logger\LoggerFactory::class)
->get('coroutine');
$logger->error("[$context] unhandled exception", [
'cid' => \Hyperf\Coroutine\Coroutine::id(),
'msg' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}
});
}
// 使用
safe_co(fn() => $this->sendNotification(), 'notify_user');
tryco