Tutorial: Share Context Across Functions
In this tutorial you will set up the async context middleware, access request context from deeply nested utility functions, build a request-scoped logger, and create a helper to get the current user from anywhere.
Prerequisites
- A Remix project with a working server (see the Getting Started guide).
- Basic understanding of
async/awaitin JavaScript.
Step 1: Add the Async Context Middleware
Place asyncContext() early in the middleware chain so all downstream code can use getContext():
import { createRouter } from 'remix/fetch-router'
import { asyncContext } from 'remix/async-context-middleware'
import { session } from 'remix/session-middleware'
import { auth } from 'remix/auth-middleware'
import { route } from 'remix/fetch-router/routes'
let routes = route({
posts: '/posts',
deletePost: '/posts/:id',
})
let router = createRouter({
middleware: [
asyncContext(),
session(sessionCookie, sessionStorage),
auth({ schemes: [sessionAuth] }),
],
})The middleware order matters. Because asyncContext() is first, every subsequent middleware and handler can call getContext().
Step 2: Create a getCurrentUser Helper
Instead of passing the user through every function, create a utility that reads it from the async context:
// 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
}Now you can call getCurrentUser() from any function during a request:
// services/posts.ts
import { getCurrentUser } from '../utils/current-user.ts'
export async function deletePost(postId: string) {
let user = getCurrentUser()
if (!user) throw new Error('Not authenticated')
// Check ownership
let post = await db.find(posts, postId)
if (post.authorId !== user.id) throw new Error('Not authorized')
await db.delete(posts, { where: eq(posts.columns.id, postId) })
}Your route handler becomes simple:
import { deletePost } from './services/posts.ts'
router.delete(routes.deletePost, async ({ params }) => {
await deletePost(params.id)
return redirect('/posts')
})Step 3: Build a Request-Scoped Logger
Create a logger that automatically includes request metadata in every log entry:
// utils/logger.ts
import { getContext } from 'remix/async-context-middleware'
export function log(level: string, message: string, extra?: Record<string, unknown>) {
let context = getContext()
let entry = {
level,
message,
method: context.request.method,
url: context.url.pathname,
timestamp: new Date().toISOString(),
...extra,
}
console.log(JSON.stringify(entry))
}Use it from anywhere:
// services/payment.ts
import { log } from '../utils/logger.ts'
export async function processPayment(orderId: string, amount: number) {
log('info', 'Processing payment', { orderId, amount })
try {
let result = await paymentGateway.charge(amount)
log('info', 'Payment succeeded', { orderId, transactionId: result.id })
return result
} catch (error) {
log('error', 'Payment failed', { orderId, error: error.message })
throw error
}
}Every log entry includes the HTTP method and URL automatically -- no need to pass them as arguments.
Step 4: Add Request Tracing
Generate a unique ID for each request and access it from anywhere:
import { createContextKey } from 'remix/fetch-router'
import { asyncContext, getContext } from 'remix/async-context-middleware'
// Define a context key for the trace ID
export let TraceId = createContextKey<string>('TraceId')
// Middleware that assigns a trace ID
function tracing(context, next) {
let traceId = context.request.headers.get('X-Trace-Id') ?? crypto.randomUUID()
context.set(TraceId, traceId)
return next()
}
let router = createRouter({
middleware: [
asyncContext(),
tracing,
],
})Now retrieve the trace ID from anywhere:
// utils/trace.ts
import { getContext } from 'remix/async-context-middleware'
import { TraceId } from '../middleware/tracing.ts'
export function getTraceId(): string {
return getContext().get(TraceId)
}Use it in your logger, error reports, or outgoing HTTP requests:
import { getTraceId } from '../utils/trace.ts'
async function callExternalApi(path: string) {
return fetch(`https://api.example.com${path}`, {
headers: {
'X-Trace-Id': getTraceId(), // Propagate trace ID to downstream services
},
})
}Step 5: Handle Missing Context Gracefully
getContext() throws an error if called outside a request (e.g., during server startup or in a background job). For utility functions that might run in both contexts, add a fallback:
import { getContext } from 'remix/async-context-middleware'
export function log(level: string, message: string) {
let requestInfo = {}
try {
let context = getContext()
requestInfo = {
method: context.request.method,
url: context.url.pathname,
}
} catch {
// Not inside a request -- that's fine
}
console.log(JSON.stringify({ level, message, ...requestInfo }))
}What You Learned
- How to set up the
asyncContextmiddleware. - How to access the current user from any function with
getContext(). - How to build a request-scoped logger that automatically includes request metadata.
- How to propagate trace IDs through the call stack and to external services.
- How to handle cases where async context is not available.
Next Steps
- API Reference -- Full details on
asyncContext()andgetContext(). - logger-middleware -- Built-in request logging middleware.
- Middleware -- How middleware works in Remix.