This tutorial walks you step by step through configuring AsyncContext in Node.js apps, propagating data across async flows, integrating with HTTP frameworks, and adopting structured logging with context.
- Installation
- First context
- Reading and writing data
- Derived contexts
- Snapshot, reset, and removal
- Framework integrations
- Structured logger
- Environment configuration
- Performance measurement
- OpenAI integration
- OpenTelemetry integration
- Sentry integration
- Best practices and pitfalls
- Quick troubleshooting
npm i @marceloraineri/async-contextCreate a context and share data across the entire async chain:
import crypto from "node:crypto";
import { Context } from "@marceloraineri/async-context";
await Context.run({ requestId: crypto.randomUUID() }, async () => {
Context.addValue("user", { id: 42, name: "Ada" });
await Promise.resolve();
console.log(Context.getValue("requestId"));
console.log(Context.getValue("user"));
});Use Context.run(() => { ... }) when you do not need an initial store.
const requestId = Context.getValue<string>("requestId");
const userId = Context.requireValue<number>("userId");
if (Context.has("tenantId")) {
// ...
}
Context.setDefault("locale", "en-US");Context.addValue("tenantId", "t_123");
Context.addObjectValue({ feature: "beta", region: "sa-east-1" });
Context.addOptions({ retry: 2, cache: true });
Context.addOptions({ flag: "canary" }, "flags");Create a child context without mutating the parent:
Context.run({ requestId: "req_1" }, () => {
Context.runWith({ jobId: "job_9" }, () => {
console.log(Context.getValue("requestId"));
console.log(Context.getValue("jobId"));
});
});const snapshot = Context.snapshot();
Context.remove("token");
Context.safeRemove("feature");
Context.reset();Context.snapshot() returns a shallow copy of the current store. Context.reset() removes all keys.
Default middleware (creates an instance_id per request):
import express from "express";
import { AsyncContextExpressMiddleware, Context } from "@marceloraineri/async-context";
const app = express();
app.use(AsyncContextExpressMiddleware);
app.get("/ping", (_req, res) => {
res.json({ instanceId: Context.getValue("instance_id") ?? null });
});Customization with idKey and seed:
import { createAsyncContextExpressMiddleware } from "@marceloraineri/async-context";
app.use(
createAsyncContextExpressMiddleware({
idKey: "request_id",
seed: (req) => ({ method: req.method, path: req.url }),
})
);import fastify from "fastify";
import { createAsyncContextFastifyHook, Context } from "@marceloraineri/async-context";
const app = fastify();
app.addHook("onRequest", createAsyncContextFastifyHook());
app.get("/ping", async () => ({ instanceId: Context.getValue("instance_id") ?? null }));import Koa from "koa";
import { createAsyncContextKoaMiddleware, Context } from "@marceloraineri/async-context";
const app = new Koa();
app.use(createAsyncContextKoaMiddleware());
app.use(async (ctx) => {
ctx.body = { instanceId: Context.getValue("instance_id") ?? null };
});import type { NextApiRequest, NextApiResponse } from "next";
import { createAsyncContextNextHandler, Context } from "@marceloraineri/async-context";
export default createAsyncContextNextHandler(
async (_req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ instanceId: Context.getValue("instance_id") ?? null });
}
);import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { AsyncContextNestMiddleware } from "@marceloraineri/async-context";
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AsyncContextNestMiddleware).forRoutes("*");
}
}// app/Http/Middleware/AsyncContext.ts
import { AsyncContextAdonisMiddleware } from "@marceloraineri/async-context";
export default AsyncContextAdonisMiddleware;The logger automatically includes the active context in each log entry (by default under the context key) and supports redaction, sampling, and timers.
import { Context, createLogger } from "@marceloraineri/async-context";
const logger = createLogger({
name: "api",
level: "info",
contextKey: "ctx",
redactKeys: ["ctx.token", "data.password"],
});
await Context.run({ requestId: "req_1", token: "secret" }, async () => {
logger.info("request started", { route: "/ping" });
});Child logger and timer:
const jobLogger = logger.child({ job: "import-users" });
const end = jobLogger.startTimer("debug");
await Promise.resolve();
end("job completed");import { createLoggerFromEnv, loggerPreset } from "@marceloraineri/async-context";
const logger = createLoggerFromEnv({
name: "api",
defaults: loggerPreset("production"),
onWarning: (warning) => {
console.warn(`[logger-env] ${warning.key}: ${warning.reason}`, {
value: warning.value,
});
},
});Key variables:
| Variable | Description | Example |
|---|---|---|
LOG_PRESET |
development, production, test |
production |
LOG_LEVEL |
minimum level (name or numeric) | info |
LOG_FORMAT |
json or pretty |
json |
LOG_CONTEXT |
attach context | true |
LOG_CONTEXT_KEY |
context key name | context |
LOG_CONTEXT_KEYS |
allowlist keys (CSV or JSON array) | requestId,tenantId |
LOG_REDACT_KEYS |
redaction paths (CSV or JSON array) | context.token,data.password |
LOG_REDACT_FIELDS |
extra sensitive fields (CSV or JSON array) | accessToken,creditCard |
LOG_BINDINGS |
JSON object or key=value pairs added to every log entry | {"service":"api","version":2} |
LOG_SAMPLE_RATE |
0..1 or percent | 0.25 |
You can also use LOGGER_ aliases (for example LOGGER_LEVEL) for every LOG_ variable.
CSV list values accept commas or semicolons as separators.
List env values are de-duplicated while preserving order.
import { Context } from "@marceloraineri/async-context";
await Context.run({}, async () => {
await Context.measure(
"db.query",
async () => {
await Promise.resolve();
},
{ data: { table: "users" } }
);
console.log(Context.getValue("perf"));
});Use key and mode to control where entries are stored:
Context.measure("cache.lookup", () => "hit", { key: "performance", mode: "overwrite" });import OpenAI from "openai";
import { Context, withOpenAIContext } from "@marceloraineri/async-context";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
await Context.run({ requestId: "req_1" }, async () => {
const response = await withOpenAIContext(
"responses.create",
{ model: "gpt-4o-mini", input: "Hello" },
(req) => openai.responses.create(req),
{ includeRequest: true }
);
console.log(response.id);
console.log(Context.getValue("openai"));
});By default, only safe request/response fields are included. You can allow extra keys with requestKeys and responseKeys.
import * as otel from "@opentelemetry/api";
import {
Context,
createAsyncContextExpressOpenTelemetryMiddleware,
setOpenTelemetryBaggageFromContext,
withOpenTelemetrySpan,
} from "@marceloraineri/async-context";
app.use(
createAsyncContextExpressOpenTelemetryMiddleware({
otel: {
api: otel,
tracerName: "api",
contextAttributeKeys: ["requestId", "tenantId"],
},
})
);
await Context.run({ requestId: "req_1", tenantId: "t_123" }, async () => {
await withOpenTelemetrySpan(
"cache.lookup",
async () => Promise.resolve("hit"),
{ api: otel, attributes: { cache: "redis" } }
);
setOpenTelemetryBaggageFromContext({
api: otel,
contextKeys: ["requestId", "tenantId"],
baggagePrefix: "ctx.",
});
});The middleware extracts trace context from incoming headers, keeps the span active for downstream handlers, and records a summary under the otel key in the async context store.
import express from "express";
import {
AsyncContextExpressMiddleware,
initSentryWithAsyncContext,
sentryAsyncContextExpressMiddleware,
sentryErrorHandler,
} from "@marceloraineri/async-context";
initSentryWithAsyncContext({
sentryInit: {
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
},
redactKeys: ["async_context.token"],
});
const app = express();
app.use(AsyncContextExpressMiddleware);
app.use(sentryAsyncContextExpressMiddleware());
app.get("/ping", (_req, res) => res.json({ ok: true }));
app.use(sentryErrorHandler());- Prefer
addValueandaddObjectValueover replacing the entire store. - Always complete middleware flows (
next()) to avoid context leaks. - Avoid storing large mutable objects in the context.
AsyncLocalStorageis per process, so each worker has its own context.- Use
contextKeysin the logger to limit exposed fields.
No active context found: you calledContext.getValueoutsideContext.runor without middleware.Context.addOptionsthrew: the target key already existed and was not an object.- Context missing in logs: check
context: trueand that a store exists.
For a quick overview, see the main README.md. This file is a step-by-step guide.