Что такое AsyncLocalStorage в Node.js и зачем он нужен?

SeniorNode.js · Backend·Обновлено 3 сентября 2026
Коротко
AsyncLocalStorage — это API из модуля node:async_hooks, который позволяет хранить данные, привязанные к текущей асинхронной цепочке выполнения (например, к обработке одного HTTP-запроса), и читать их из любого места этой цепочки без явной передачи через аргументы функций.

Суть механизма

AsyncLocalStorage (модуль node:async_hooks) реализует аналог thread-local storage, но для асинхронного мира Node.js. Она создаёт «контекст выполнения», который автоматически распространяется через все асинхронные операции (промисы, setTimeout, callbacks, fs, сетевые запросы), порождённые внутри вызова .run(). Это позволяет получить доступ к данным контекста в любой точке цепочки — без явной передачи через каждый слой приложения.

Под капотом используется низкоуровневый API async_hooks: когда создаётся новый асинхронный ресурс, Node.js связывает его с родительским контекстом исполнения. AsyncLocalStorage использует эту связь, чтобы «протащить» store через всю цепочку.

API

  • new AsyncLocalStorage() — создание хранилища.
  • .run(store, callback, ...args) — выполняет callback (и всё, что из него асинхронно порождено) в контексте store.
  • .getStore() — возвращает текущий store или undefined, если вызван вне .run().
  • .enterWith(store) — привязывает контекст к текущему выполнению без обёртки в callback; менее безопасен, так как контекст «утекает» дальше по времени жизни события и его сложнее корректно завершить.
  • .disable() — сбрасывает все активные контексты.

Зачем нужен

  1. Request-scoped данные в серверах — traceId, requestId, userId из JWT можно положить в контекст на входе в middleware и читать в логгере, сервисах и репозиториях любого уровня вложенности, не протаскивая их через сигнатуры функций.
  2. Трейсинг и APM — OpenTelemetry, Sentry, Datadog используют AsyncLocalStorage, чтобы автоматически связывать логи и спаны с текущим запросом.
  3. Замена паттерна «передавай context явно» — особенно полезно в больших кодовых базах, где нет DI-контейнера с request-scope.
  4. Транзакционный контекст — например, привязка активной транзакции БД к цепочке вызовов, чтобы вложенные repository-методы автоматически её использовали.

Пример: requestId в логах

import { AsyncLocalStorage } from 'node:async_hooks';
import http from 'node:http';
import { randomUUID } from 'node:crypto';

const als = new AsyncLocalStorage<{ requestId: string }>();

function log(message: string) {
  const store = als.getStore();
  // requestId подставляется автоматически, без передачи параметром
  console.log(`[${store?.requestId ?? 'no-context'}] ${message}`);
}

async function handleBusinessLogic() {
  log('обработка запроса началась');
  await new Promise((r) => setTimeout(r, 10));
  log('обработка запроса завершена');
}

http.createServer((req, res) => {
  const requestId = randomUUID();
  als.run({ requestId }, async () => {
    await handleBusinessLogic();
    res.end('ok');
  });
}).listen(3000);

Подводные камни

  • Если код вызывает getStore() вне цепочки, запущенной через .run(), вернётся undefined — нужно предусматривать fallback.
  • enterWith проще, но может привести к «протеканию» контекста между несвязанными операциями на одном event loop tick — в большинстве случаев предпочтителен run.
  • Контекст не передаётся автоматически между worker_threads — нужно сериализовать нужные данные через postMessage.
  • Есть небольшой overhead на каждую асинхронную операцию из-за необходимости отслеживать граф контекстов; в hot path с миллионами операций в секунду это стоит измерить.
  • Долго живущие callbacks, зарегистрированные внутри run() (например, подписка на EventEmitter), могут держать store в памяти дольше, чем ожидается, создавая риск утечек.

Что хочет услышать интервьюер

Кандидат понимает, что AsyncLocalStorage — это контекст, привязанный к асинхронной цепочке выполнения, а не обычная глобальная переменная или singleton

Знает основной API: run(), getStore(), enterWith() и разницу в их поведении и рисках

Может привести практический кейс — request-scoped логирование/трейсинг, передача requestId/userId через middleware без явных параметров

Понимает, что механизм построен поверх async_hooks и распространяется через промисы, таймеры и callbacks

Знает ограничения: поведение вне run(), отсутствие автоматической передачи контекста в worker_threads, риски утечек при enterWith

Пример: Базовое использование run/getStore

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage<{ userId: string }>();

function getCurrentUserId(): string | undefined {
  return als.getStore()?.userId;
}

async function processOrder() {
  // где-то глубоко внутри асинхронной цепочки,
  // без передачи userId параметром
  console.log('Текущий пользователь:', getCurrentUserId());
}

als.run({ userId: 'user-42' }, async () => {
  await processOrder();
});

console.log('Вне контекста:', getCurrentUserId()); // undefined

Пример: Express middleware с requestId и Winston-логгером

import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
import express from 'express';
import winston from 'winston';

const als = new AsyncLocalStorage<{ requestId: string }>();

const logger = winston.createLogger({
  format: winston.format.printf(({ message }) => {
    const requestId = als.getStore()?.requestId ?? 'system';
    return `[${requestId}] ${message}`;
  }),
  transports: [new winston.transports.Console()],
});

const app = express();

app.use((req, res, next) => {
  const requestId = randomUUID();
  // всё, что произойдёт внутри next(), включая асинхронные вызовы,
  // будет иметь доступ к requestId через als.getStore()
  als.run({ requestId }, next);
});

app.get('/orders/:id', async (req, res) => {
  logger.info('Получение заказа началось');
  // сервис/репозиторий на любом уровне вложенности может вызвать logger.info
  // и requestId подставится автоматически
  res.json({ id: req.params.id });
});

app.listen(3000);

Типичные ошибки

Путают AsyncLocalStorage с обычной глобальной переменной или модульным singleton, не понимая, что контекст изолирован для каждой асинхронной цепочки

Вызывают getStore() вне run() и не могут объяснить, почему возвращается undefined

Используют enterWith вместо run без понимания рисков утечки контекста между операциями

Не знают, что контекст не передаётся автоматически в worker_threads и требует ручной сериализации

Считают, что AsyncLocalStorage сам по себе решает проблему конкурентного доступа к shared state, путая её с многопоточной синхронизацией

Лучшие курсы по теме

изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Node.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее