hyperf 中子协程(co/go)抛出的未捕获异常默认不被全局 exceptionhandler 捕获,会直接终止协程且不冒泡;必须在子协程内手动 try/catch 并记录日志,不可依赖外部处理器。

Hyperf 中子协程(如 co、go、Coroutine::create)抛出的未捕获异常,**默认不会被全局 ExceptionHandler 捕获**——它会直接终止该协程,且不冒泡到父协程或 HTTP 请求生命周期中。这是协程调度模型决定的,不是配置遗漏。
为什么子协程异常不走全局 ExceptionHandler
Hyperf 的全局异常处理器(如 App\Exception\Handler\AppExceptionHandler)只接管「HTTP 请求主协程」中未被捕获的异常。而 co 或 go 启动的是独立协程,其异常由 Swoole 协程调度器直接处理,不经过 Hyperf 的请求上下文和异常分发链。
- 错误现象:子协程内
throw new RuntimeException('xxx'),日志里只有PHP Fatal error: Uncaught RuntimeException,但AppExceptionHandler::handle完全没执行 - 根本原因:子协程与 HTTP 请求协程是平行关系,不是父子继承关系;异常无法跨协程边界自动传播
- 影响:这类异常不会记录到
exceptions.php配置的日志处理器中,也不会返回标准错误响应,容易漏监控
必须在子协程内手动 try/catch
这是最直接、最可控的解法。不能依赖外部捕获,必须在协程启动的闭包内部兜底。
- 正确写法:每个
co/go闭包第一层就加try/catch,并显式记录或上报 - 别偷懒写成
co(function () { /* 大段逻辑 */ });—— 里面任何位置抛异常都会静默失败 - 建议统一用
Hyperf\Logger\LoggerFactory记录,带上coroutine_id()方便追踪
示例:
use Hyperf\Logger\LoggerFactory;
use Hyperf\Coroutine\Coroutine;
<p>$logger = $this->container->get(LoggerFactory::class)->get('job');</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill4029" title="Hyperframes Creative"><img
src="https://img.php.cn/upload/skill/000/000/081/178988960574411.jpg" alt="Hyperframes Creative" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill4029" title="Hyperframes Creative" class="overflowclass">Hyperframes Creative</a>
<p class="overflowclass">HyperFrames视频非动画创意指导,包括设计规范(frame.md/design.md)处理、配色、字体设计、旁白及节奏规划等。</p>
</div>
<a rel="nofollow" href="/xiazai/skill4029" title="Hyperframes Creative" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>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 等
}
});
</p>
避免用 set_exception_handler 替代
有人试图在子协程里调用 set_exception_handler,这是无效的:
-
set_exception_handler是 PHP 进程级的,对协程无感知,Swoole 协程中触发时不会进入该回调 - Hyperf 的
ExceptionHandler机制本身也不作用于子协程,它是基于Hyperf\HttpServer\Middleware\ErrorMiddleware绑定在请求生命周期上的 - 强行注册反而可能污染 Worker 进程的全局状态,导致后续请求异常处理错乱
进阶:封装安全协程启动工具
如果项目中大量使用子协程,重复写 try/catch 易出错。可封装一个安全版启动函数:
function 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(),
]);
}
});
}
<p>// 使用
safe_co(fn() => $this->sendNotification(), 'notify_user');
</p>
注意:这个封装不能替代业务层的精准错误处理,仅作为兜底。真正需要重试、降级或用户提示的逻辑,仍得在 try 块内明确实现。
子协程异常不冒泡是协程模型的固有特性,不是 bug。指望全局处理器一劳永逸地覆盖所有协程,只会让问题更隐蔽——日志缺失、监控断点、线上故障难复现。最稳妥的做法,就是把每个 co 当作一个微型“请求”来对待:自己负责初始化、自己兜底异常、自己打标日志。










