Request Context

Request-Scoped Context with AsyncLocalStorage

Three calls deep, the code that wants to log a request id no longer has req. The usual answers are both bad: thread req through every signature down to the database layer, or keep the id in a module variable, which is wrong the moment two requests overlap. Node's AsyncLocalStorage (AsyncLocalStorage) instead carries a store along the asynchronous call chain, so any function running for a request can read it. One middleware creates the store and runs the rest of the chain inside it.

A request context every module can readJavaScript
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
export const context = new AsyncLocalStorage();
export const log = (msg) => console.log(`[${context.getStore()?.requestId ?? '-'}] ${msg}`);
app.use((req, res, next) => {
  const store = { requestId: req.get('x-request-id') ?? randomUUID() };
  res.set('X-Request-Id', store.requestId);
  context.run(store, next);           // every later layer runs inside this store
});

Two overlapping requests, tagged req-a and req-b by the client, interleave their work and still label every line correctly, although log is called from a data-access function that was never passed req:

Output of 25
[req-b] loading book 2
[req-a] loading book 1
[req-b] loaded book 2
[req-a] loaded book 1

Register this middleware first, before the logger, so even a body-parse failure is logged with an id. Honor an incoming x-request-id — that is how a trace survives across a gateway and several services — but cap its length and strip control characters before it reaches your logs.

Keep the store small: a request id, the authenticated user id, a tenant, a start time. AsyncLocalStorage is cheap in modern Node but not free, and a store holding an open database connection keeps that object alive for the whole request. Correlation IDs builds correlation ids in structured logs on this same context.