В production это нужно в Node.js-сервисах, SSR, API integration и backend-for-frontend слоях, где requestId, traceId, tenant или транзакция должны проходить через async-код. Частая ошибка - протаскивать
ctx через десятки методов или, наоборот, прятать в нём бизнес-состояние.Как это выглядит
AsyncLocalStorage использует async_hooks и привязывает store к async execution flow: promise, timeout, I/O callback и большинству стандартных API Node.js.type Ctx = { requestId: string; traceId?: string; tx?: unknown };
const als = new AsyncLocalStorage<Ctx>();
app.use((req, _res, next) => {
als.run({
requestId: req.headers['x-request-id']?.toString() ?? randomUUID(),
traceId: req.headers.traceparent?.toString(),
}, next);
});
const getCtx = () => als.getStore();
function logInfo(msg: string, meta = {}) {
const c = getCtx();
logger.info({
requestId: c?.requestId,
traceId: c?.traceId,
...meta,
}, msg);
}Теперь сервисный код не знает про HTTP middleware и
req, но логи автоматически получают correlation metadata.Транзакции без протаскивания tx
Для DB слоя можно делать
withTransaction(), который запускает als.run({ ...ctx, tx }, fn), а getDbExecutor() возвращает ctx.tx ?? db. Trade-off хороший: меньше шума в API сервисов, но граница ответственности остаётся инфраструктурной, а не бизнесовой.Где границы
* не заменяйте аргументы функции: бизнес-данные передавайте явно;
* не кладите в store
req, res, большие payload или ORM graph;* context не пересекает process boundary: worker threads, очереди, cron jobs и другие сервисы требуют явной передачи ids;
* нестандартные callback-based библиотеки могут разорвать async chain.
Практическое правило: используйте
AsyncLocalStorage для логов, tracing, tenant/user metadata, аудита и текущей транзакции, а не для скрытого глобального состояния.Вывод:
AsyncLocalStorage полезен, когда request-scoped инфраструктурный контекст улучшает observability и DX, но не размывает явные границы бизнес-логики.