A request identifier is useful in logs only if the right identifier follows the right request through asynchronous work. A global currentRequest variable fails when another request changes it while the first awaits a database or network call. Passing a context object through every helper is explicit but can make deeply layered logging cumbersome. AsyncLocalStorage offers another option: establish an asynchronous scope and retrieve its store from descendant work. This Node 24 guide uses concurrent in-memory tasks to demonstrate isolation, then callback binding to make an intentionally retained context visible. It does not require Express or an external tracing library.
Create a scope with run
Construct an AsyncLocalStorage instance for the kind of context you want to manage. Call run with a store and a callback. Within that callback and supported asynchronous work created by it, getStore returns the associated store. Outside a scope, getStore can return undefined, so a logger should have a sensible fallback or an explicit requirement for context. run returns the callback's result, including its promise when the callback is async.
Use one long-lived storage instance for the application facility rather than creating a new instance for every request. Create a fresh store object for each request. A request middleware can establish the scope before calling the next handler, and a service function can retrieve the request ID when recording an event. Keep authorization decisions based on a trustworthy authenticated context, not merely on a user-supplied request ID placed in the store. An identifier helps correlation; it is not proof of identity or permission.
Keep stores distinct and intentionally small
AsyncLocalStorage keeps scopes separate, but it does not deep-clone store values. If two runs receive the same mutable object, their code still shares that object. An array or nested object placed in a store can likewise be mutated by several descendants of one request. Use immutable or carefully owned values when possible. A frozen small object containing a request ID and tenant label is easier to reason about than a mutable bag holding an entire request and response.
Long-lived callbacks or unfinished asynchronous resources can retain a store longer than expected. Avoid storing large bodies, credentials, database clients, or every transient object merely because context access is convenient. Context should support the operation, not become an unbounded per-request cache. Review lifecycle when scheduling work that outlives a request. A background task may need a copied correlation identifier or a new context instead of retaining the original request's full data and implied authority indefinitely.
Understand propagation and context boundaries
Promises, timers, and ordinary supported asynchronous operations generally preserve the context of the scope in which their work is created. The main fixture awaits different timer delays in two scopes and then reads the stores. Their completion order differs, but each retrieves its own ID. Context is tied to the asynchronous execution chain rather than whichever task happened to start most recently. A plain global variable cannot supply that behavior.
Custom callback systems, unusual thenables, or code invoking callbacks in a different execution context can expose propagation boundaries. Diagnose where getStore first becomes undefined instead of adding global fallback state. AsyncLocalStorage.bind or snapshot can capture an intended context for later supported use, and AsyncResource is available for more complex integration. Worker threads and separate processes do not automatically inherit a parent's store as though they shared one request stack; transmit required data explicitly and establish context at the receiving boundary. Distributed tracing also needs a defined propagation protocol between services.
Prefer scoped entry over implicit ambient changes
run clearly encloses the lifetime and origin of a context. enterWith changes the current synchronous context and can affect subsequent work in the same event execution path, which makes accidental leakage easier when several handlers share a dispatcher. Prefer run when a bounded request or task callback can express the intended scope. Nested runs can temporarily provide a different store while the outer scope remains available after the nested callback returns.
Do not use context storage to conceal essential dependencies that a function's interface should make explicit. A logging request ID is a reasonable ambient concern; a function silently selecting a database or granting access from an unexpected store can be harder to test and audit. Test concurrent scopes, missing context, nested context, and any callback boundary your real application uses. Deterministic tests can return contextual values instead of asserting a fragile interleaved log order. The fixture uses Promise.all to preserve result order while still running both tasks concurrently.
Node.js documentation and version notes
Node.js AsyncLocalStorage Request Context Examples
Each example shows the code, the output it produces and what each part does.
Isolating request IDs across concurrent tasks
Run the complete .mjs example on Node 24. Each task receives a separate frozen store. Task A waits longer than task B, but the returned array follows the input order supplied to Promise.all. The nested helper reads the store after the await, proving that the ID follows the asynchronous scope rather than a synchronous variable assignment. A final getStore call occurs outside either scope and prints undefined. The demonstration does not claim that console output from arbitrary concurrent tasks always appears in request order; it collects results first precisely to avoid that unsupported claim.
import { AsyncLocalStorage } from 'node:async_hooks';
import { setTimeout as delay } from 'node:timers/promises';
const context = new AsyncLocalStorage();
function currentId() { return context.getStore()?.requestId; }
function task(requestId, wait) {
return context.run(Object.freeze({ requestId }), async () => {
await delay(wait);
return currentId();
});
}
const results = await Promise.all([task('A', 15), task('B', 1)]);
console.log(JSON.stringify(results));
console.log(context.getStore());["A","B"]
undefinedNode.js AsyncLocalStorage Request Context in Real Projects
Compare bound and unbound callbacks across scopes
Capture a callback within one scope, then invoke it from another scope. Compare an ordinary function with a function wrapped by AsyncLocalStorage.bind. The ordinary function reads the context at invocation time, while the bound function runs in its captured context. The solution creates both inside an outer scope named source and calls both inside a scope named caller. This exercise clarifies that a JavaScript closure over the storage instance is not itself a context capture. Bind intentionally when a later callback should retain its originating scope, and consider whether that retained scope should be smaller than a complete request object.
import { AsyncLocalStorage } from 'node:async_hooks';
const context = new AsyncLocalStorage();
let ordinary;
let captured;
context.run({ requestId: 'source' }, () => {
ordinary = () => context.getStore()?.requestId;
captured = AsyncLocalStorage.bind(ordinary);
});
context.run({ requestId: 'caller' }, () => {
console.log(ordinary());
console.log(captured());
console.log(context.getStore().requestId);
});
console.log(context.getStore());caller
source
caller
undefinedNode.js AsyncLocalStorage Request Context FAQ
Does AsyncLocalStorage copy the store for me?
No. It associates the object you provide with a scope. Reusing the same mutable object in several scopes still shares those mutations. Create a fresh store per request and prefer small immutable values when practical. A frozen top-level object does not automatically freeze nested objects, so consider the ownership of nested state separately. Scope isolation is not a general-purpose deep-copy mechanism.
Why can getStore return undefined?
The current execution may be outside an established scope, or a callback boundary may not preserve the context you intended. A general logger can use a clear fallback label, while a context-required operation should fail explicitly. Investigate the first boundary where context disappears. Do not repair it by writing a global current-request variable, which restores the concurrency problem the storage was meant to solve.
Does context automatically cross worker threads?
No. Workers and separate processes need explicit data transfer. Send the required correlation fields with the task message, then establish a scope in the receiving execution environment. The same applies between services, where a documented tracing or request-correlation protocol carries data across the network. Keep sensitive authorization context distinct from untrusted propagation headers and validate it at the receiving boundary.
When is bind useful?
It is useful when a callback invoked later should run under the context captured when it was bound. The practice compares that behavior with an ordinary function reading the invocation context. Binding can also retain store data longer, so capture only an appropriate scope and release long-lived references. For more complex custom asynchronous integrations, consult AsyncResource rather than assuming one wrapper solves every lifecycle issue.
Node.js AsyncLocalStorage Request Context Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What does the bound callback print when invoked inside caller in the worked example?
Node.js AsyncLocalStorage Request Context Quiz
2 questions to check what you learned.
Why is a global currentRequestId unsafe during overlapping awaits?
- Another request can overwrite it before the first resumes
- await automatically copies every global variable
- Node forbids all global variables inside servers
Why keep context stores small?
- Long-lived async work can retain their data beyond the request
- AsyncLocalStorage rejects stores with more than one property
- Small stores automatically authenticate request IDs