协程中“捕获未抛出的错误”是认知误区,context仅传递数据而不捕获错误;需通过try/catch重抛异常、统一异常处理器、显式传递上下文及绑定日志追踪来确保错误可见可溯。

Hyperf协程中“捕获未抛出的程序错误”,本质不是技术动作,而是认知误区——协程上下文(Context)本身不负责捕获错误,它只负责隔离和传递数据;错误捕获必须靠 try/catch + 正确异常传播机制。很多开发者误以为把用户ID、trace_id塞进Context,就能“兜住”逻辑错误或静默失败,结果导致bug隐蔽、重试失效、日志断链。
真正要解决的,是两类典型场景:
Context 无法掩盖的“未抛出错误”
-
验证失败但没返回 errors 字段:不是 Context 没传,而是 ValidationExceptionHandler 配置错位,server name 和 exceptions.php 中 handler 键名不一致(比如 server 名是
api,但配置写成了'http' => [...]),导致异常根本没被正确处理器接管。 -
Job 任务静默失败:handle() 方法里写了
try { ... } catch (\Exception $e) { logger()->error($e); }却没throw $e,框架认为任务成功,重试机制完全失效。 -
Redis pipeline 中途异常退出:调用
$redis->pipeline()后抛异常,又没在 finally 里 exec 或 reset,连接泄漏 → 后续协程卡在WaitTimeoutException,看起来像“错误没被捕获”,实则是资源层阻塞掩盖了原始错误。
正确做法:让错误浮出水面,再用 Context 辅助定位
-
所有业务逻辑入口强制包裹 try/catch,且 catch 中必须重抛
public function handle() { try { $this->doBusiness(); } catch (\Throwable $e) { // 记录关键上下文,再重抛 $userId = Context::get('user_id') ?? 'unknown'; logger()->error('Job failed', [ 'job' => static::class, 'user_id' => $userId, 'trace_id' => Context::get('trace_id'), 'exception' => $e, ]); throw $e; // ✅ 触发重试、进入异常处理器 } } -
验证类异常必须走标准流程
不要手动if (!$validator->passes()) return ...,而应让ValidationException自然抛出,由ValidationExceptionHandler统一响应,确保errors字段结构化输出。 -
异步任务中主动注入上下文,再触发错误
子协程启动前,显式取父协程 Context 数据并 set 进去:$traceId = Context::get('trace_id'); $userId = Context::get('user_id'); Coroutine::create(function () use ($traceId, $userId) { Context::set('trace_id', $traceId); Context::set('user_id', $userId); // 此处出错,日志和异常都能关联原始请求 throw new \RuntimeException('test error'); });
日志与追踪必须绑定协程生命周期
- 在中间件或控制器入口,统一设置:
Context::set('trace_id', $request->getAttribute('request_id') ?: uniqid('req_')); Context::set('correlation_id', uniqid()); - 日志 formatter 启用堆栈(
include_stacktraces => true),打印时带上:$this->logger->info('order created', [ 'cid' => Coroutine::getCid(), 'trace_id' => Context::get('trace_id'), 'user_id' => Context::get('user_id'), ]);
Context 是数据管道,不是错误保险丝。错误不会因为放进 Context 就自动被捕获或修复,它只会因 Context 传得准,才更容易被发现和归因。











