Under The Hood
backend nodejs Node.js 20+ (async_hooks)

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

react equivalent
N/A — Node.js runtime 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.

before → after
Manual context threading
async function handler(req, res, requestId) {
  await serviceA(requestId); // ❌ OLD: requestId passed everywhere
  await serviceB(requestId);
}
AsyncLocalStorage ambient context
const als = new AsyncLocalStorage();
// ✅ NEW: run per request; als.getStore() works anywhere in the async tree

Interview 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.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.