My day-job is Staff Software Engineer & Engineering Manager.
Building courses.reviews as a hobby
AsyncLocalStorage: Simplify Context Management in Node.js
How AsyncLocalStorage solves context management in asynchronous Node.js apps
AsyncLocalStorage gives you a way to maintain context across your async operations without manually passing data through every function. Think of it like having a secret storage box that follows your request around, carrying important information that any part of your code can access.
Here’s what a typical Express application without AsyncLocalStorage might look like. We need to pass the userId through multiple functions:
Notice how we keep passing userId everywhere? Now multiply this by several more parameters like requestId, tenantId, and locale. The function signatures grow unwieldy fast.
Here’s how we can clean this up with AsyncLocalStorage
The context follows the request through its entire lifecycle. No more parameter passing.
Info
AsyncLocalStorage requires Node.js 23 or higher, so anyone still on older versions will need to use the --experimental-async-context-frame flag.
When to Use AsyncLocalStorage
Here’s where AsyncLocalStorage shines: tracking requests as they flow through your microservices. You can log the request ID, trace ID, and other metadata without passing them through every function.
AsyncLocalStorage keeps track of the current transaction across your database operations. This is useful when you need to pass the transaction object through multiple functions:
AsyncLocalStorage is also useful for logging. You can store the current log context and retrieve it in any function.
When you run this, each log message automatically includes the full request context:
When Not to Use AsyncLocalStorage
While AsyncLocalStorage is powerful, I avoid it when:
The context only needs to flow through a couple of functions - regular parameter passing is clearer.
Working with synchronous code - AsyncLocalStorage adds unnecessary complexity.
When you build a public API using AsyncLocalStorage, you’re forcing a specific way of managing context onto your users. Consider a payment processing API:
Your API consumers now must wrap every call in storage.run(), which might not fit their application’s architecture.
They can’t simply call processPayment() directly with a user ID. Instead of writing straightforward code like await processor.processPayment(100, userId), they’re forced into this more complex pattern:
A more flexible approach would make the context optional while still supporting AsyncLocalStorage
This approach gives API consumers the freedom to choose their preferred approach while maintaining compatibility with AsyncLocalStorage when needed. They can either use the context storage or pass parameters directly, fitting their specific use case and architecture.
AsyncLocalStorage might seem like magic at first, but understanding its boundaries helps you use it effectively. When used appropriately, it significantly cleans up your code and makes context management a breeze.