What is AsyncLocalStorage?
AsyncLocalStorage is a powerful feature in Node.js that allows you to create and manage asynchronous contexts. It provides a way to store data that is local to an asynchronous operation, and have that data automatically propagated across various async boundaries like Promises, callbacks, and async/await, without needing to manually pass it through every function call.
What is AsyncLocalStorage?
In asynchronous programming, maintaining context (e.g., a request ID, a user ID, or a transaction object) across a series of non-blocking operations can be challenging. Traditional approaches often involve passing context objects explicitly through function arguments, which can lead to 'prop drilling' and cluttered code.
AsyncLocalStorage (introduced in Node.js v13.10.0, stable in v14.5.0) addresses this by providing a mechanism to store data and make it accessible throughout the entire asynchronous flow that originates from a specific point. It's similar in concept to 'thread-local storage' in synchronous multi-threaded environments, but adapted for Node.js's single-threaded, event-driven model.
It acts like a map where you can store any serializable data (objects, strings, numbers) that will be automatically available to any code executed within the same asynchronous context.
How it Works
The core of AsyncLocalStorage revolves around two main methods: run() and getStore().
asyncLocalStorage.run(store, callback, ...args): This method creates a new asynchronous context. Thestoreargument is the data (any value) you want to make available within this context. Thecallbackfunction is executed immediately within this new context. Any asynchronous operations initiated withincallback(or functions called bycallback) will inherit this context.asyncLocalStorage.getStore(): This method retrieves thestorevalue that was set by the nearestrun()call in the current asynchronous execution chain. If norun()call is active in the current chain, it returnsundefined.
Node.js internally manages the propagation of this context using its event loop and promise tracking mechanisms. When an async operation (like a Promise, setTimeout, fs.readFile) is scheduled, the current AsyncLocalStorage context is captured and re-established when that operation's callback or .then() handler is executed.
Basic Usage Example
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
function logWithContext(message) {
const store = asyncLocalStorage.getStore();
console.log(`[Request ID: ${store ? store.requestId : 'N/A'}] ${message}`);
}
async function simulateRequest(requestId) {
logWithContext('Starting request');
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate async work
logWithContext('First async step complete');
// Another async operation
await Promise.resolve().then(() => {
logWithContext('Second async step complete (inside then)');
});
logWithContext('Request finished');
}
// Simulate two concurrent requests
asyncLocalStorage.run({ requestId: 'REQ-123' }, () => {
simulateRequest('REQ-123');
});
asyncLocalStorage.run({ requestId: 'REQ-456' }, () => {
simulateRequest('REQ-456');
});
// Code outside any run() call
logWithContext('Global context check');
Key Concepts and Best Practices
- Isolation: Each
run()call creates an independent context. Asynchronous operations started within onerun()context will not interfere with or access the data from anotherrun()context, even if they execute concurrently. - Immutability: It's best practice to store immutable data or deep copies in the
store. If you store mutable objects, modifications within one part of the async flow could unexpectedly affect other parts that retrieve the same mutable object. - Performance: AsyncLocalStorage has minimal overhead for typical usage. It's highly optimized internally, making it suitable for high-throughput applications.
- Error Handling: If an error occurs within a
run()callback, the context is still properly managed and released. It doesn't leak or prevent other contexts from functioning correctly. - Alternatives: For very simple cases, explicit function arguments or closures might still be clearer. AsyncLocalStorage shines when context needs to traverse many layers of async calls and different modules without being explicitly passed everywhere.
When to Use AsyncLocalStorage
- Request Tracing/Logging: Injecting a unique request ID into all log messages generated during an HTTP request's lifecycle.
- User Context: Making the authenticated user's ID or permissions available to various database operations or service calls without explicitly passing it.
- Transaction Management: Passing a database transaction object through multiple data access layers, ensuring all operations within a request occur within the same transaction.
- Distributed Tracing: Propagating correlation IDs across microservices.
Conclusion
AsyncLocalStorage is a powerful and essential tool for building robust and maintainable Node.js applications, especially in complex server-side scenarios. It elegantly solves the problem of context propagation across asynchronous operations, reducing boilerplate code and improving readability, while providing strong isolation guarantees.