AsyncLocalStorage 仅在当前异步调用链中有效,需在每个异步入口显式调用 run() 包裹,否则 getStore() 返回 undefined;Express 中间件必须将 next() 置于 run() 回调内,错误处理与日志也须同域执行。
AsyncLocalStorage 不是“全局变量”,也不是“跨请求共享存储”,它只在当前异步调用链中有效。只要中间没漏掉
,就能让
在任意深度的
、
、
、数据库回调里稳定返回原始上下文。
Express 中间件必须用
包裹整个请求生命周期
常见错误是只在中间件里调用
,但没把
放进回调里 —— 这会导致后续路由、控制器、服务层完全脱离上下文。
✅ 正确写法:在
回调内执行
,确保所有后续异步操作都在该存储作用域内
❌ 错误写法:
——
在作用域外执行,
返回
⚠️ 注意:如果用了
或自定义错误处理中间件,也要确保错误处理逻辑同样运行在
作用域内,否则
里的
会是
返回
的三个高频原因
不是 API 失效,而是异步链被意外切断。最常发生在:
第三方库内部新建了 Promise(如某些老版本
的 query 方法未正确延续 async context)
使用了
或
且未显式包裹
—— 它们虽属异步,但不自动继承 AsyncLocalStorage 上下文
在
或
全局监听器中直接调用
—— 这些事件已脱离原始请求链,必须靠外部透传(如从
取值)
日志格式化时别直接拼接
结果
Winston、Pino 等日志库的 format 函数可能在任意时刻同步执行,而
只在异步调用链中有效。若日志写入发生在非请求上下文中(比如定时任务、健康检查),
就是
。
推荐做法:在中间件中将
注入
或
,日志 format 里优先 fallback 到该字段
避免写
—— 看似安全,但掩盖了上下文丢失问题,不利于排查链路断裂点
更健壮的日志封装示例:
AsyncLocalStorage 的可靠性高度依赖“每个异步入口都显式 run”这一纪律。它不会自动穿透所有异步边界,尤其对底层 C++ 插件、原生流事件或手动创建的微任务,需要主动补位。一旦发现某处
失效,不要绕过它加 fallback,先查那里是不是漏了
或用了不兼容的异步模式。
asyncLocalStorage.run()getStore()awaitPromise.thensetTimeoutasyncLocalStorage.run()run()next()run()next()asyncLocalStorage.run(...); next();next()getStore()undefinedexpress-async-errorsrun()reportError(err, context?.requestId)contextundefinedgetStore()undefinedmysql2setImmediate()process.nextTick()run()uncaughtExceptionunhandledRejectiongetStore()req.idgetStore()getStore()getStore()undefinedrequestIdres.localsreq.id`${getStore()?.requestId || 'N/A'}`const log = (msg, meta = {}) => {
const store = asyncLocalStorage.getStore();
const id = store?.requestId ?? req?.id ?? 'unknown';
logger.info(`[${id}] ${msg}`, meta);
};getStore()run()