hyperf协程mysql异常未进全局exceptionhandler,因db异常发生在非http上下文(如队列、定时任务),不经过http层级handler,须注册到exception数组并精准匹配pdoexception/queryexception、不吞异常、调用stoppropagation()终止传播。

Hyperf协程MySQL异常没进全局ExceptionHandler?
不是配置漏了,是MySQL异常根本没走到HTTP异常处理器里——因为async-queue、定时任务、WebSocket等非HTTP上下文里的DB异常,压根不经过HttpExceptionHandler。你配再全的AppExceptionHandler,对DB::select()里抛出的PDOException也无能为力。
- 协程环境下
PDOException仍会正常抛出,但触发点不在HTTP生命周期内,config/autoload/exceptions.php中http层级的handler完全不生效 - 真正起作用的是
exception层级(非http),需显式注册到该数组下,例如:'exception' => [App\Exception\Handler\MySqlExceptionHandler::class] - 别把
MySqlExceptionHandler放在http数组里,否则它永远收不到DB异常
如何写一个真正生效的MySQL异常Handler
必须满足三个条件:匹配类型精准、不吞异常、主动终止传播链。下面是最简可用模板:
namespace App\Exception\Handler;
use Hyperf\Database\Exception\QueryException;
use Hyperf\ExceptionHandler\ExceptionHandler;
use PDOException;
use Throwable;
class MySqlExceptionHandler extends ExceptionHandler
{
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof PDOException || $throwable instanceof QueryException;
}
public function handle(Throwable $throwable, \Psr\Http\Message\ResponseInterface $response)
{
// 记录结构化日志,含SQL、绑定参数、连接信息
logger()->error('MySQL exception', [
'sql' => $throwable->getPrevious()?->getMessage() ?? '',
'message' => $throwable->getMessage(),
'code' => $throwable->getCode(),
]);
// 不返回响应(非HTTP上下文无$response可用),只记录+终止传播
$this->stopPropagation();
}
}
-
isValid()必须严格限定为PDOException或QueryException,不能写return true,否则会误吞其他异常 -
handle()方法里不要调用$response->withStatus()——在队列/定时任务里$response是null,会报错 - 必须调用
$this->stopPropagation(),否则异常继续上抛,可能被更宽泛的Throwablehandler二次处理,导致重复日志或掩盖根因
为什么try-catch包DB操作反而让异常消失?
在Job类handle()或Commandhandle()里写try { DB::insert(...); } catch (\Exception $e) { },结果异常既没日志也没重试,是因为你主动“消化”了异常。
- Hyperf异步队列重试机制只响应未捕获的异常,
catch块里空着或只打日志不throw,框架认为任务执行成功 - 正确做法是:记录日志后立即
throw $e,或用throw new RuntimeException('xxx', 0, $e)包装原异常保留堆栈 - 若需降级逻辑(如DB失败改查缓存),应在
catch里完成降级,最后仍要throw——否则重试和failed队列全部失效
协程安全的DB异常捕获要注意什么
Hyperf的DB组件虽默认协程化,但异常发生时若混用同步IO(如file_put_contents写日志)、或在finally里调用非协程安全的Redis::close(),会导致协程卡死或静默退出。
- 日志务必用
logger()->error()而非error_log()或file_put_contents(),前者已协程安全封装 - 避免在
catch块里调用DB::connection()->reconnect()这类阻塞操作,协程环境下它不会自动让出控制权 - 如果要用
Redis辅助记录失败上下文,优先用$redis->withPipeline()而非裸pipeline(),防止连接泄漏导致后续异常无法记录
真正麻烦的从来不是写个try-catch,而是判断异常该在哪一层被捕获、是否该继续上抛、以及协程环境下哪些操作表面正常实则已破坏执行流。这些点漏掉任何一个,全局捕获就变成全局失明。











