What is AsyncLocalStorage in Node.js and How to Use It

This article explains the purpose of the AsyncLocalStorage class in Node.js and demonstrates how it propagates transactional context across asynchronous execution paths. You will learn how this API acts as the Node.js equivalent to thread-local storage, allowing you to track request-specific data—such as correlation IDs, user sessions, or database transactions—throughout the lifecycle of an asynchronous operation without relying on prop drilling.

The Purpose of AsyncLocalStorage

In multi-threaded environments, developers use Thread-Local Storage (TLS) to store data specific to the current thread of execution. However, Node.js runs on a single-threaded event loop, using callbacks, promises, and async/await to handle concurrency. Because multiple concurrent operations share the same thread, traditional TLS cannot be used to keep track of state for individual requests.

Historically, passing context (like a user ID or transaction token) meant passing arguments through every function call in the execution chain—a tedious process known as “prop drilling.” AsyncLocalStorage (part of the built-in node:async_hooks module) solves this problem. It provides a way to store and retrieve state that is bound to the lifetime of a specific asynchronous control flow, making the data accessible to any function called within that flow.

How AsyncLocalStorage Propagates Context

AsyncLocalStorage works by tapping into the internal asynchronous tracking mechanisms of Node.js. When an asynchronous operation is initiated, Node.js links the parent asynchronous context with the child asynchronous context.

The lifecycle of propagating context with AsyncLocalStorage involves three main steps:

  1. Instantiation: You create an instance of AsyncLocalStorage.
  2. Context Binding (run): You use the run() method to initialize the store with a specific value (the context) and execute a callback function. All code executed within this callback—including nested asynchronous operations like database queries, HTTP requests, or timers—will have access to this store.
  3. Retrieval (getStore): Any function executed within the asynchronous boundary can call the getStore() method to retrieve the context.

Code Example: Request Tracking

Below is a practical example demonstrating how AsyncLocalStorage propagates a correlation ID across multiple asynchronous boundaries without passing it as an argument.

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

// 1. Instantiate the storage
const asyncLocalStorage = new AsyncLocalStorage();

// Simulated database operation
function saveToDatabase() {
  // 3. Retrieve the context from anywhere in the call chain
  const context = asyncLocalStorage.getStore();
  console.log(`[DB] Saving data for Request ID: ${context?.requestId}`);
}

// Simulated service layer
async function processBusinessLogic() {
  console.log('[Service] Processing business logic...');
  // Simulating an asynchronous delay (e.g., API call)
  await new Promise((resolve) => setTimeout(resolve, 100));
  saveToDatabase();
}

// Simulated HTTP server request handler
function handleRequest(user) {
  const requestId = randomUUID();
  const context = { requestId, user };

  // 2. Run the asynchronous chain inside the context
  asyncLocalStorage.run(context, async () => {
    console.log(`[Server] Received request. Assigned ID: ${requestId}`);
    await processBusinessLogic();
  });
}

// Trigger two concurrent requests
handleRequest('Alice');
handleRequest('Bob');

Explanation of Propagation in the Example

When handleRequest('Alice') is called, a unique request ID is generated. The asyncLocalStorage.run method binds this ID to the execution flow.

Even though processBusinessLogic and saveToDatabase do not accept the request ID as an argument, and even though they run asynchronously after a setTimeout delay, Node.js internally propagates the context. When getStore() is called inside saveToDatabase(), Node.js resolves the asynchronous execution path back to the execution context of the specific request, returning the correct ID for both Alice and Bob concurrently.