Skip to main content
Async Context provides request-scoped state that automatically flows through your async operations—no prop drilling required. It’s built on Node.js’s AsyncLocalStorage, giving you a clean way to carry data like request IDs, user sessions, or trace information through your entire call stack.
Node.js Only: Async Context requires Node.js’s AsyncLocalStorage API and is not available in browser or edge runtimes. For non-Node environments, pass context explicitly through function parameters.

When to Use Async Context

Async Context excels at carrying cross-cutting concerns through your application:
  • Request tracing: Carry a requestId or traceId through all operations
  • User sessions: Access the current user without passing it everywhere
  • Database transactions: Share a transaction across multiple operations
  • Logging context: Automatically include request metadata in all logs

Basic Usage

Define a context, provide values at the entry point, and read from anywhere in the call stack:
import { r, run } from "@bluelibs/runner";

// 1. Define your context shape
const requestContext = r
  .asyncContext<{ requestId: string; userId?: string }>("app.ctx.request")
  .build();

// 2. Wrap your request handler
async function handleRequest(req: Request) {
  await requestContext.provide(
    { requestId: crypto.randomUUID() },
    async () => {
      // Everything inside here can access the context
      await processRequest(req);
    }
  );
}

// 3. Read from anywhere in the call stack
async function processRequest(req: Request) {
  const ctx = requestContext.use();
  console.log(`Processing request ${ctx.requestId}`);
}

Using Context in Tasks

The real power comes when you inject context into your tasks:
const auditLog = r
  .task("app.tasks.auditLog")
  .dependencies({ requestContext, logger: globals.resources.logger })
  .run(async (message: string, { requestContext, logger }) => {
    const ctx = requestContext.use();
    await logger.info(message, {
      requestId: ctx.requestId,
      userId: ctx.userId,
    });
  })
  .build();

// Register the context alongside your tasks
const app = r
  .resource("app")
  .register([requestContext, auditLog])
  .build();

API Reference

defineAsyncContext

Create a new typed async context.
definition
object
required
Context configuration

Context Methods

provide
function
Establish a context scope with the given value
provide<R>(value: T, fn: () => Promise<R> | R): Promise<R> | R
All async operations within fn can access the context via use().
use
function
Read the current context value
use(): T
Throws ContextError if called outside a provide() scope.
require
function
Return middleware that enforces context availability
require(): ITaskMiddlewareConfigured
Tasks with this middleware will throw if executed outside a context scope.
optional
function
Mark this context as an optional dependency
optional(): { inner: IAsyncContext<T>; [symbolOptionalDependency]: true }

Requiring Context with Middleware

Force tasks to run only within a context boundary:
const securedTask = r
  .task("app.tasks.secured")
  .middleware([requestContext.require()]) // Throws if context not provided
  .run(async (input) => {
    const ctx = requestContext.use(); // Guaranteed to exist
    return { processedBy: ctx.userId };
  })
  .build();

Express Integration Example

import express from "express";
import { r, run } from "@bluelibs/runner";

const requestContext = r
  .asyncContext<{ requestId: string; userId?: string }>("app.ctx.request")
  .build();

const app = express();

// Middleware to establish context for each request
app.use((req, res, next) => {
  requestContext.provide(
    {
      requestId: req.headers["x-request-id"] || crypto.randomUUID(),
      userId: req.user?.id,
    },
    () => next()
  );
});

// Any task called during this request can access the context
app.get("/api/data", async (req, res) => {
  const result = await runtime.runTask(fetchData, req.params);
  res.json(result);
});

Custom Serialization

By default, Runner preserves Dates, RegExp, and other types across async boundaries. For custom types:
interface User {
  id: string;
  name: string;
  createdAt: Date;
}

const sessionContext = r
  .asyncContext<{ user: User }>("app.ctx.session")
  .serialize((data) => JSON.stringify({
    user: {
      ...data.user,
      createdAt: data.user.createdAt.toISOString(),
    },
  }))
  .parse((raw) => {
    const parsed = JSON.parse(raw);
    return {
      user: {
        ...parsed.user,
        createdAt: new Date(parsed.user.createdAt),
      },
    };
  })
  .build();

Validation with Zod

Validate context values at runtime:
import { z } from "zod";

const requestSchema = z.object({
  requestId: z.string().uuid(),
  userId: z.string().optional(),
  tenantId: z.string(),
});

const requestContext = r
  .asyncContext<z.infer<typeof requestSchema>>("app.ctx.request")
  .configSchema(requestSchema)
  .build();

// This will throw validation error
requestContext.provide(
  { requestId: "invalid-uuid", tenantId: "tenant-123" },
  async () => {
    // Won't reach here
  }
);

Multi-Tenant Example

const tenantContext = r
  .asyncContext<{ tenantId: string; permissions: string[] }>("app.ctx.tenant")
  .build();

const getTenantData = r
  .task("app.tasks.getTenantData")
  .dependencies({ db, tenantContext })
  .middleware([tenantContext.require()])
  .run(async (params, { db, tenantContext }) => {
    const { tenantId } = tenantContext.use();
    return db.query("SELECT * FROM data WHERE tenant_id = ?", [tenantId]);
  })
  .build();

// Usage
await tenantContext.provide(
  { tenantId: "tenant-123", permissions: ["read", "write"] },
  async () => {
    const data = await runtime.runTask(getTenantData, {});
    // Data automatically scoped to tenant-123
  }
);

Error Handling

import { ContextError } from "@bluelibs/runner";

try {
  // Attempting to use context outside a provide() scope
  const ctx = requestContext.use();
} catch (error) {
  if (error instanceof ContextError) {
    console.error("Context not available:", error.message);
  }
}

Best Practices

Establish at Entry Points

Call provide() at HTTP handlers, queue consumers, or other entry points

Keep Contexts Focused

Create separate contexts for different concerns (request, tenant, auth)

Use require() for Critical Paths

Force context availability with .require() middleware for security-sensitive tasks

Avoid Context Mutation

Treat context values as immutable; create new scopes with provide() for changes

Performance Notes

  • Context lookup via use() is extremely fast (~microseconds)
  • Context values are not copied; they’re stored by reference
  • Nested provide() calls create new context scopes without affecting parent scopes
  • Validation only runs when configSchema is provided

Platform Detection

Runner automatically detects platform capabilities:
import { getPlatform } from "@bluelibs/runner";

const platform = getPlatform();
if (!platform.hasAsyncLocalStorage()) {
  console.warn("Async Context not available on this platform");
}
Attempting to create async context on unsupported platforms throws platformUnsupportedFunctionError.

See Also

Build docs developers (and LLMs) love