Tracking Context Across Async Boundaries
In this lesson, you will learn how to maintain state across asynchronous operations using the AsyncLocalStorage API.
When you write asynchronous code in Node.js, the execution flow often jumps between callbacks, promises, and event loop phases. If you need to keep track of a "context"—such as a unique request ID, a database transaction, or a logged-in user—passing that data manually through every single function argument becomes messy and error-prone. This is known as "prop-drilling."
AsyncLocalStorage solves this by acting as a "storage container" that follows the logical flow of your code, even when it pauses for I/O and resumes later. It ties your data to the asynchronous execution lifecycle rather than the call stack.
How it works
Think of AsyncLocalStorage as a invisible pocket you carry around. You can put something in the pocket at the start of a request, and anywhere within that specific execution chain, you can reach in and retrieve it without having to pass it as a parameter.
const { AsyncLocalStorage } = require('async_hooks');
const storage = new AsyncLocalStorage();
function logWithId(message) {
const id = storage.getStore();
console.log(`[Request ${id}] ${message}`);
}
// Start a context
storage.run('123', () => {
logWithId('Starting database query...');
setTimeout(() => {
// The ID "123" persists even inside this async callback
logWithId('Query finished.');
}, 1000);
});
When you call storage.run(data, callback), Node.js ensures that the provided data is associated with the callback and any subsequent asynchronous operations spawned by it. When the setTimeout eventually fires during the event loop's "Timers" phase, the runtime is smart enough to "re-attach" that specific context, allowing you to access the original ID.
This is a powerful way to implement cross-cutting concerns like logging, tracing, or security authorization without polluting your function signatures with boilerplate data.
Key Takeaway: AsyncLocalStorage allows you to store and access context data across asynchronous operations by linking the data to the execution lifecycle instead of the physical call stack.