AsyncLocalStorage в Node.js решает задачу передачи данных о текущем запросе (например, ID пользователя, токена авторизации, настроек локали) между асинхронными функциями без необходимости передавать их явно через каждый уровень вызова. Это механизм контекста, аналогичный ThreadLocal в Java или Context в Go, но специфичный для событийной модели Node.js. Он хранит данные в стеке, который привязан к текущему потоку выполнения (thread), что позволяет безопасно использовать его с библиотеками вроде MongoDB, Redis или ORM.

Что такое AsyncLocalStorage и принцип его работы в Node.js

AsyncLocalStorage — это встроенный класс Node.js (доступен с версии 14.17.0), предназначенный для хранения данных, привязанных к текущему потоку выполнения. В отличие от глобальных переменных или объектов, хранящихся в замыкании модуля, данные в AsyncLocalStorage изолированы между разными запросами.

Принцип работы строится на стеке (stack). Каждый вызов getStore() создает новый уровень стека. Вы можете добавлять данные через set(key, value), и они будут доступны всем функциям, вызванным внутри этого контекста, пока стек не очистится. Если функция вызывает другую асинхронную функцию (например, через Promise или callback) в том же потоке, она автоматически наследует текущий стек данных.

Важно: данные хранятся только в памяти текущего потока. Если вы используете пул потоков (worker threads), контекст не передается между ними автоматически. Также, если вы используете библиотеки, которые создают новые потоки для выполнения задач (например, некоторые реализации Web Workers или специфичные драйверы БД), данные из AsyncLocalStorage в них не будут доступны без явной передачи.

Основное преимущество — чистота кода. Вам не нужно передавать объект с данными о запросе (req) как аргумент во все функции обработки данных. Вы просто вызываете getStore() и получаете доступ к данным, которые были установлены для этого конкретного запроса.

Сценарий использования: передача контекста между асинхронными операциями

Типичный сценарий: обработка HTTP-запроса. Вы получаете объект req, извлекаете ID пользователя или токен авторизации и сохраняете их в AsyncLocalStorage. Затем вы вызываете базу данных, отправляете email или логируете событие. Все эти операции могут быть асинхронными (возвращают Promise или используют callbacks), но они выполняются в том же потоке, поэтому имеют доступ к данным контекста.

Пример: функция getUserData() вызывает getStore(). Если данные не установлены, она возвращает null или ошибку 401. Это позволяет легко реализовать middleware-цепочку, где каждый шаг может проверять наличие данных без передачи их явно.

Сценарий с ошибками: если в процессе обработки возникает исключение (throw), стек данных автоматически очищается при завершении выполнения стека вызовов. Однако, если вы используеты try/catch и хотите сохранить данные для отладки или логирования после сбоя, вам нужно явно управлять очисткой или использовать финализаторы.

Ограничение: AsyncLocalStorage не работает с асинхронными операциями, которые создают новые потоки (например, через stream.pipeline в некоторых случаях или worker_threads). В таких ситуациях данные контекста теряются.

Практическая реализация: создание кастомного middleware для обработки запросов

Для интеграции с Express или Fastify создайте middleware, который инициализирует контекст при начале обработки запроса. В этом middleware вы извлекаете данные из req (например, userId из заголовка Authorization) и сохраняете их в AsyncLocalStorage.

Пример кода для Express:

const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

app.use((req, res, next) => {
const userId = req.headers['x-user-id']; // или из токена
if (userId) {
asyncLocalStorage.run({ userId }, () => {
next();
});
} else {
next();
}
});

Внутри обработчиков маршрутов вы можете вызывать asyncLocalStorage.getStore() для получения userId. Это позволяет легко добавлять логику авторизации, персонализации или аудита в любом месте кода.

Для очистки контекста после завершения запроса используйте метод exit(). В Express это можно сделать через middleware на конце стека (например, с помощью app.use((req, res) => { … })).

Управление жизненным циклом хранилища: вход, выход и очистка данных

Метод getStore() возвращает текущий контекст. Если стек пуст, возвращается null. Метод set(key, value) добавляет данные в стек. Метод clear() удаляет все данные из стека.

Важно: метод exit() вызывается автоматически при завершении выполнения всех асинхронных операций внутри текущего контекста. Однако, если вы используете try/catch и хотите гарантировать очистку даже при ошибке, используйте finally блок или Promise.finally().

Пример управления жизненным циклом:

async function processRequest() {
const store = asyncLocalStorage.getStore();
if (!store) throw new Error('No context available');

try {
// Логика обработки запроса
await someAsyncOperation();
} catch (error) {
console.error('Error processing request:', error);
// Данные автоматически очищаются при завершении стека,
// но можно явно вызвать clear() если нужно сбросить состояние раньше
}
}

Ограничение: данные в AsyncLocalStorage не сохраняются между запросами. Они существуют только во время выполнения текущего потока и удаляются после завершения всех асинхронных операций.

Обработка ошибок и сброс состояния при исключениях

При возникновении ошибки в асинхронной функции, которая использует AsyncLocalStorage, стек данных автоматически очищается после завершения выполнения всех операций. Это означает, что если вы вызовете getStore() после ошибки, вы получите null.

Если вам нужно сохранить данные для отладки или логирования после сбоя, используйте try/catch и явно вызывайте clear() в блоке catch или finally.

Пример:

async function handleRequest() {
const store = asyncLocalStorage.getStore();
if (!store) return;

try {
await riskyOperation();
} catch (error) {
console.error('Error:', error);
// Данные уже будут очищены, но можно явно сбросить если нужно
asyncLocalStorage.clear();
}
}

Важно: не полагайтесь на автоматическую очистку для критических операций. Явно вызывайте clear() после завершения обработки запроса, чтобы избежать утечек памяти или состояния.

Ограничения использования: синхронные вызовы и границы контекста

AsyncLocalStorage работает только в пределах одного потока. Если вы используете библиотеки, которые создают новые потоки (например, worker_threads), данные контекста не передаются автоматически.

Синхронные вызовы внутри асинхронной функции наследуют контекст, но если вы вызываете async/await функцию, она создает новый стек данных. Это означает, что данные из родительского контекста не будут доступны в дочерней функции, если она не использует getStore() явно.

Пример:

async function parent() {
asyncLocalStorage.set('key', 'value');
await child(); // child не видит 'key' автоматически
}

Для передачи данных между потоками используйте другие механизмы (например, MessageChannel или передачу через каналы IPC).

Ограничение: AsyncLocalStorage не поддерживает передачу контекста между разными процессами (cluster mode). Каждый процесс имеет свой собственный стек данных.

Миграция с Session/Middleware-подхода на AsyncLocalStorage

Если вы используете session middleware (например, express-session) или передаёте req объект во все функции, миграция на AsyncLocalStorage упростит код. Вместо передачи req в каждую функцию, вы сохраняете нужные данные в контекст.

Шаги миграции:

1. Определите, какие данные из req нужно передать (userId, locale, permissions).

2. Создайте middleware для инициализации AsyncLocalStorage с этими данными.

3. Удалите передачу req в функции обработки данных.

4. Замените вызовы getStore() на использование данных из контекста.

5. Добавьте очистку контекста после завершения запроса.

Преимущества: код становится чище, легче поддерживается и масштабируется. Вы можете легко изменить логику передачи данных без изменения всех функций.

Практические примеры и ошибки

Ошибка: использование AsyncLocalStorage с worker_threads. Если вы используете worker_threads для параллельной обработки, данные контекста не передаются автоматически. Решением является передача данных через каналы IPC или использование других механизмов.

Ошибка: игнорирование очистки контекста. Если вы не вызывайте clear() после завершения запроса, данные могут накапливаться в памяти и вызывать утечки. Всегда вызывайте clear() после завершения обработки запроса.

Ошибка: попытка использовать AsyncLocalStorage с асинхронными операциями, которые создают новые потоки (например, некоторые реализации stream.pipeline). В таких случаях данные контекста теряются.

Вопросы и ответы

Как проверить, что данные из AsyncLocalStorage доступны в асинхронной функции?

Данные автоматически наследуются во всех асинхронных функциях внутри того же потока. Просто вызовите getStore() в нужной функции и проверьте наличие данных.

Можно ли использовать AsyncLocalStorage с библиотеками, которые создают новые потоки?

Нет, данные контекста не передаются автоматически в новые потоки. Для таких случаев используйте другие механизмы передачи данных (например, MessageChannel или IPC).

Как гарантировать очистку контекста при ошибке?

Используйте try/catch и вызывайте clear() в блоке catch или finally. Также можно использовать Promise.finally() для гарантии очистки.

По теме

const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

app.use((req, res, next) => {
  const userId = req.headers['x-user-id'];
  if (userId) {
    asyncLocalStorage.run({ userId }, () => {
      next();
    });
  } else {
    next();
  }
});

app.get('/profile', (req, res) => {
  const store = asyncLocalStorage.getStore();
  if (!store) return res.status(401).send('Unauthorized');
  res.json({ userId: store.userId });
});