AsyncLocalStorage: Context Propagation Across Async/Await Hops
- Last updated
- Prerequisites:
- Node.js async/await basics
- Understanding of the event loop and async context
- nodejs
- async-local-storage
- async-hooks
- context
- tracing
- request-id
Read at your depth
The practical view
AsyncLocalStorage lets you store data that follows the async context: const als = new AsyncLocalStorage(); als.run(store, () => { ... }); — inside the run callback (and any async work it spawns, no matter how many awaits), als.getStore() returns the store. It's the standard way to propagate request IDs, user IDs, and tracing contexts through async code without threading parameters through every function. It's built on async_hooks (the runtime's async context tracking). Use it for logging (inject the request id into every log line), error reporting, and per-request configuration. Node's built-in http server, database drivers, and frameworks (Fastify, NestJS) support or use it.
The same idea in other frameworks
N/A — Node.js runtime concern.N/A — Node.js concern.N/A — Node.js concern.N/A — Node.js concern.Legacy vs modern
Threading context through params vs AsyncLocalStorage
Passing requestId through every function is invasive and error-prone; AsyncLocalStorage makes it ambient across the whole async chain.
async function handler(req, res, requestId) {
await serviceA(requestId); // ❌ OLD: requestId passed everywhere
await serviceB(requestId);
}const als = new AsyncLocalStorage();
// ✅ NEW: run per request; als.getStore() works anywhere in the async treeInterview gotchas
Context
This tests the async_hooks mechanism — interviewers want the resource-based context capture/restore and the honest gaps (worker_threads, detached callbacks, performance).
The mechanical answer
AsyncLocalStorage captures the current context when an async resource (Promise, Timer, socket, stream) is CREATED within a run() scope, and restores it when that resource's callbacks execute — so the store is available across arbitrary await chains, setInterval callbacks, event handlers, and streams spawned in the same context. Limitations: it does NOT cross worker_threads (each worker has its own ALS store — you must re-establish context in the worker), it doesn't propagate to callbacks that escape the context in ways the runtime can't track (e.g. a Promise created before run() but resolved after), and there is a measurable performance cost (context switch at every async boundary — the async_hooks machinery is disabled when no ALS is active, so the cost only appears where ALS is used).
Trap
The naive answer is 'ALS works across everything'. It does not cross worker_threads, and context captured in one async resource's creation doesn't magically apply to unrelated detached callbacks. Another trap: using ALS per microtask (e.g. inside a tight loop) — the overhead multiplies; the pattern is one run() per request boundary. Candidates who list the worker/performance caveats show real experience.
Context
This probes the abstraction level — interviewers want the raw init/before/after events vs the high-level store API and the safety/ergonomics argument.
The mechanical answer
The raw async_hooks API exposes lifecycle events: 'init' (an async resource was created), 'before' (its callback is about to run), 'after', 'destroy', and 'promiseResolve'. You build your own context tracking with these, which is powerful but dangerous: writing it wrong can cause context leaks (wrong store in callbacks), memory leaks (not tracking resource destruction), or infinite loops (async hooks that create async resources). AsyncLocalStorage is the high-level wrapper built on this machinery: it gives you run()/getStore()/enterWith() and handles the capture/restore and resource-lifetime bookkeeping correctly. The guidance is to always use AsyncLocalStorage instead of raw async_hooks for context propagation — raw async_hooks is for specialized tooling and comes with a stability warning.
Trap
The naive answer is 'ALS is just a nicer API for async_hooks'. It's also a SAFER one — raw async_hooks misused can corrupt context or leak memory. Another trap: thinking you can freely combine raw async_hooks with ALS without interference — both use the same underlying resource-tracking machinery, and mixing them carelessly causes context bleed. Candidates who explain the raw-event model and the ALS safety wrapper show real depth.