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
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.
let router = createRouter({
middleware: [
asyncContext(),
],
})getContext()
Returns the current request context from AsyncLocalStorage. Call this from anywhere in the call stack during a request.
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:
- Creates an
AsyncLocalStorageinstance. - When a request arrives, runs the rest of the middleware chain and handler inside
asyncLocalStorage.run(), passing the request context. - 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
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
// 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
}// 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:
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
// 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:
// 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
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():
let router = createRouter({
middleware: [
asyncContext(),
logger(),
compression(),
session(sessionCookie, sessionStorage),
auth({ schemes: [sessionAuth] }),
],
})Caveats
- Node.js only ---
AsyncLocalStorageis a Node.js API. This middleware does not work in environments withoutAsyncLocalStoragesupport (some edge runtimes). - Not for background tasks --- If you spawn a background task that outlives the request (for example, using
setTimeoutafter the response is sent),getContext()may not work or may return stale data. - Performance ---
AsyncLocalStoragehas a small performance overhead. For most applications this is negligible, but high-throughput services should benchmark.
Related
- Middleware --- How middleware works in Remix.
- logger-middleware --- Logging that benefits from async context.