What you will build

A global current-request variable is unreliable when requests interleave around await. A slow request can resume after another has overwritten that variable. Use AsyncLocalStorage to associate a small context object with each asynchronous chain, allowing a deeply called logging helper to recover the correct ID without receiving it as a parameter.

This executable example models overlapping requests using real promise timers. It records start and resume events, checks their IDs and verifies the caller has no request context afterward. It then tests a failing request and a logging call outside any context.

Prerequisites

A supported Node.js release with native ES modules and AsyncLocalStorage. Tested on Node.js v26.13.0. No HTTP server, logging package or npm installation is required. Save as context.mjs.

import assert from 'node:assert/strict';
import {AsyncLocalStorage} from 'node:async_hooks';
import {setTimeout as sleep} from 'node:timers/promises';

const contexts = new AsyncLocalStorage();
const events = [];
function record(stage) {
  const context = contexts.getStore();
  if (!context) throw new Error('missing request context');
  events.push(`${context.requestId}:${stage}`);
}
async function request(requestId, delay, fail = false) {
  return contexts.run(Object.freeze({requestId}), async () => {
    record('start');
    await sleep(delay);
    record('resume');
    if (fail) throw new Error('fixture failure');
    return contexts.getStore().requestId;
  });
}
const result = await Promise.all([request('A', 20), request('B', 1)]);
assert.deepEqual(result, ['A', 'B']);
assert.deepEqual([...events].sort(), ['A:resume', 'A:start', 'B:resume', 'B:start']);
assert.equal(contexts.getStore(), undefined);
await assert.rejects(request('C', 1, true), /fixture failure/);
assert.equal(contexts.getStore(), undefined);
assert.throws(() => record('outside'), /missing request context/);
console.log('isolated A/B contexts; failure restored caller; outside rejected');

Run and expected output

node context.mjs
isolated A/B contexts; failure restored caller; outside rejected

How it works

The request function enters a context with run(), passing a fresh frozen object. Every record call obtains the current store using getStore(). The official AsyncLocalStorage reference describes how context is available to asynchronous work created inside the callback.

The delays differ to encourage interleaving, but assertions do not depend on scheduler order. Sorting a copy of the events verifies the complete set of start and resume records. The return-value assertion separately verifies that Promise.all associates each result with its original request.

The failing request logs within its context and rejects. After awaiting that failure, the outer caller still sees undefined. The record helper treats missing context as an error, exposing accidental out-of-request logging. A production logger could instead attach an explicit background-task identity; that should be deliberate rather than a stale request ID.

Edge cases and scope

The context is shallow-frozen and contains only a string, which is sufficient here. Adding nested mutable objects would allow changes inside them despite freezing the outer object. Keep stores small and avoid attaching large payloads or secrets merely for logging convenience.

This is asynchronous context propagation, not durable storage. It does not send IDs across HTTP calls, queues, workers or processes. Those boundaries need explicit propagation and validation. A background task spawned inside a request can inherit that context; decide whether that lifetime is appropriate before detaching work.

Troubleshooting

If a library loses the store, reduce the path to native promises and timers like this fixture. The official documentation discusses callback adapters and AsyncResource for custom asynchronous boundaries. Do not repair lost context by assigning a shared global ID. If imports fail, check the .mjs suffix. These checks inspect caller context after success and rejection, so failures should not be dismissed as merely logging issues.

Primary references

Continue with node.js tutorials and all Java Samples categories.