Skip to content

async-context-middleware

The asyncContext middleware uses Node.js AsyncLocalStorage to propagate the request context through the entire call stack. This lets you access the current request context from any function --- database utilities, logging helpers, third-party integrations --- without explicitly passing it through every function argument.

Installation

The async context middleware is included with Remix. No additional installation is needed.

Import

ts
import { asyncContext, getContext } from 'remix/async-context-middleware'

API

asyncContext()

Returns a middleware function that stores the request context in AsyncLocalStorage for the duration of the request.

ts
let router = createRouter({
  middleware: [
    asyncContext(),
  ],
})

getContext()

Returns the current request context from AsyncLocalStorage. Call this from anywhere in the call stack during a request.

ts
import { getContext } from 'remix/async-context-middleware'

function getCurrentUser() {
  let context = getContext()
  return context.get(Auth)
}

Throws an error if called outside of a request context (for example, during server startup or in a background task that was not initiated within a request).

How It Works

Node.js AsyncLocalStorage provides a way to store data that is available throughout the lifetime of an asynchronous operation. The middleware:

  1. Creates an AsyncLocalStorage instance.
  2. When a request arrives, runs the rest of the middleware chain and handler inside asyncLocalStorage.run(), passing the request context.
  3. Any function called during that request can retrieve the context with getContext().

This works across await boundaries, setTimeout, Promise chains, and any other asynchronous continuation.

Examples

Basic Usage

ts
import { createRouter } from 'remix/fetch-router'
import { asyncContext, getContext } from 'remix/async-context-middleware'
import { Auth } from 'remix/auth-middleware'

let router = createRouter({
  middleware: [
    asyncContext(),
    session(sessionCookie, sessionStorage),
    auth({ schemes: [sessionAuth] }),
  ],
})

Accessing Context in Utility Functions

ts
// utils/current-user.ts
import { getContext } from 'remix/async-context-middleware'
import { Auth } from 'remix/auth-middleware'

export function getCurrentUser() {
  let context = getContext()
  let authState = context.get(Auth)
  if (!authState.ok) return null
  return authState.identity
}
ts
// utils/audit-log.ts
import { getCurrentUser } from './current-user.ts'

export async function auditLog(action: string, details: Record<string, unknown>) {
  let user = getCurrentUser()
  await db.create(auditLogs, {
    userId: user?.id ?? null,
    action,
    details: JSON.stringify(details),
    timestamp: new Date(),
  })
}

Now you can call auditLog() from anywhere without passing the request context:

ts
router.map(deletePostRoute, async ({ params }) => {
  await db.delete(posts, { where: eq(posts.columns.id, params.id) })
  await auditLog('delete_post', { postId: params.id })
  return redirect('/posts')
})

Request-Scoped Logging

ts
// utils/logger.ts
import { getContext } from 'remix/async-context-middleware'

export function log(level: string, message: string) {
  let context = getContext()
  let requestId = context.get(RequestId)
  let method = context.request.method
  let url = context.url.pathname

  console.log(JSON.stringify({
    level,
    message,
    requestId,
    method,
    url,
    timestamp: new Date().toISOString(),
  }))
}

Every log entry includes the request ID and URL, even from deeply nested functions:

ts
// services/payment.ts
import { log } from '../utils/logger.ts'

export async function processPayment(amount: number) {
  log('info', `Processing payment of $${amount}`)
  // ...
  log('info', 'Payment processed successfully')
}

Request Tracing

ts
import { asyncContext, getContext } from 'remix/async-context-middleware'
import { createContextKey } from 'remix/fetch-router'

export const TraceId = createContextKey<string>('TraceId')

function tracing(context, next) {
  let traceId = context.request.headers.get('X-Trace-Id') ?? crypto.randomUUID()
  context.set(TraceId, traceId)
}

// Anywhere in your code
function getTraceId() {
  return getContext().get(TraceId)
}

Middleware Order

Place asyncContext() early in the chain so that all downstream middleware and handlers have access to getContext():

ts
let router = createRouter({
  middleware: [
    asyncContext(),
    logger(),
    compression(),
    session(sessionCookie, sessionStorage),
    auth({ schemes: [sessionAuth] }),
  ],
})

Caveats

  • Node.js only --- AsyncLocalStorage is a Node.js API. This middleware does not work in environments without AsyncLocalStorage support (some edge runtimes).
  • Not for background tasks --- If you spawn a background task that outlives the request (for example, using setTimeout after the response is sent), getContext() may not work or may return stale data.
  • Performance --- AsyncLocalStorage has a small performance overhead. For most applications this is negligible, but high-throughput services should benchmark.

Released under the MIT License.