协程内未捕获的异常不会向上传播,只终止当前协程;必须在每个协程内部用 try/catch 捕获,或通过 Channel 传递、全局 setExceptionHandler 兜底,否则导致静默崩溃或空白响应。

协程内未捕获的 Exception 不会向上传播到父协程或主流程,只终止当前协程 —— 这是和同步 PHP 最根本的区别,也是绝大多数线上异常“静默丢失”的根源。
协程不能跨层级捕获异常
在普通 PHP 中,try/catch 可以包裹任意函数调用链;但在 Swoole 协程里,一旦进入新协程(比如 go() 或 Coroutine::create()),就形成了独立的执行上下文。父协程的 try 对子协程抛出的异常完全无效。
- 错误写法:
try { go(function() { throw new RuntimeException(); }); } catch (Throwable $e) { }——$e永远不会被捕获 - 正确写法:每个
go()内部必须自己加try/catch - 例外情况:仅当使用
Coroutine::setExceptionHandler()设置了全局处理器时,未捕获异常才会被该回调兜底,但此时已无法做业务级响应(如返回 HTTP 500)
协程客户端异常必须在协程内处理
Swoole\Coroutine\Http\Client、Swoole\Coroutine\MySQL 等协程客户端在 I/O 失败时会直接抛出 RuntimeException 或子类,而不是返回 false。这些异常若不在当前协程内捕获,协程立即退出,但 Worker 进程继续运行,请求看似“成功”实则无响应。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 典型表现:HTTP 接口偶发空白响应、超时、状态码 200 但 body 为空
- 必须检查每个协程内的客户端调用后是否可能抛异常,尤其是
$client->get()、$mysql->query()、$redis->get() - 不要依赖
if (!$client->connected)这类判断,底层连接失败往往直接 throw,不走返回值分支
Throwable 覆盖范围比 Exception 更广
Swoole 协程中推荐始终用 catch (\Throwable $e),而非 catch (\Exception $e)。因为:
-
\Error类型(如FatalError、)也继承自 <code>\Throwable,而它们无法被\Exception捕获 - 协程切换过程中某些底层错误(如内存不足、栈溢出)会以
\Error形式抛出 -
exit()在协程中会抛出\Swoole\ExitException,它也是\Throwable的子类,需显式处理才能避免协程意外终止
日志和监控容易漏掉协程异常
协程异常默认不触发 PHP 的 set_exception_handler(),也不写入 error_log,除非你主动记录。很多团队只配置了 FPM 下的错误日志,却没为协程单独设置日志路径或格式。
- 建议在每个
go()的catch块里调用error_log()或写入结构化日志(如 JSON 格式含协程 ID、trace) - 配合
Coroutine::getContext()获取当前协程 ID,便于追踪同一请求下的多个协程行为 - 注意:
error_log()默认写入stderr,在 Docker 或 systemd 环境下可能被截断或丢弃,应明确指定文件路径
真正棘手的不是“怎么捕获”,而是“在哪捕获”——协程入口点(go()、onRequest 回调、onReceive)必须是第一道防线,任何嵌套调用都不应假设外层会兜底。

















