0
# Async Context Support
1
2
Async context support enables maintaining context across asynchronous boundaries, solving the problem of context loss after `await` statements in JavaScript.
3
4
## API Reference
5
6
```typescript { .api }
7
/**
8
* Wrapper for async functions requiring context preservation
9
* Shows warning if function is not transformed by build plugin
10
* @param fn - Async function that needs context preservation
11
* @param transformed - Internal flag indicating if function was transformed
12
* @returns The same function, potentially enhanced for context preservation
13
*/
14
function withAsyncContext<T = any>(
15
fn: () => Promise<T>,
16
transformed?: boolean
17
): () => Promise<T>;
18
19
/**
20
* Execute async function with context restoration helpers
21
* @param fn - Async function to execute
22
* @returns Tuple of [promise, restore function] for manual context restoration
23
*/
24
function executeAsync<T>(
25
fn: () => Promise<T>
26
): [Promise<T>, () => void];
27
28
interface ContextOptions {
29
/**
30
* Enable native async context support using AsyncLocalStorage
31
*/
32
asyncContext?: boolean;
33
34
/**
35
* AsyncLocalStorage implementation for async context
36
*/
37
AsyncLocalStorage?: typeof AsyncLocalStorage;
38
}
39
```
40
41
## The Async Context Problem
42
43
By default, context is lost after the first `await` statement:
44
45
```typescript
46
import { createContext } from "unctx";
47
48
const userContext = createContext<User>();
49
50
// ❌ Context is lost after await
51
userContext.call(userData, async () => {
52
console.log(userContext.use()); // ✅ Works - before await
53
54
await fetch("/api/data");
55
56
console.log(userContext.tryUse()); // ❌ Returns null - after await
57
});
58
```
59
60
## Solutions
61
62
### 1. Native AsyncLocalStorage (Node.js/Modern Runtimes)
63
64
Use Node.js AsyncLocalStorage for native async context support:
65
66
```typescript
67
import { createContext } from "unctx";
68
import { AsyncLocalStorage } from "node:async_hooks";
69
70
interface RequestContext {
71
requestId: string;
72
userId: number;
73
}
74
75
const requestContext = createContext<RequestContext>({
76
asyncContext: true,
77
AsyncLocalStorage
78
});
79
80
// ✅ Context persists across async boundaries
81
await requestContext.callAsync(
82
{ requestId: "req-123", userId: 42 },
83
async () => {
84
console.log(requestContext.use().requestId); // "req-123"
85
86
await fetch("/api/users");
87
88
// ✅ Context still available after await
89
console.log(requestContext.use().requestId); // "req-123"
90
91
await processNestedAsync();
92
}
93
);
94
95
async function processNestedAsync() {
96
// ✅ Context available in nested async functions
97
const ctx = requestContext.use();
98
await new Promise(resolve => setTimeout(resolve, 100));
99
console.log(ctx.requestId); // "req-123"
100
}
101
```
102
103
### 2. Build-time Transformation
104
105
Use build plugins to automatically transform async functions:
106
107
```typescript
108
import { withAsyncContext } from "unctx";
109
110
const userContext = createContext<User>();
111
112
// Mark function for transformation
113
const processUser = withAsyncContext(async () => {
114
console.log(userContext.use()); // ✅ Available
115
116
await fetch("/api/data");
117
118
// ✅ Context restored automatically by transform
119
console.log(userContext.use()); // ✅ Available after await
120
});
121
122
await userContext.callAsync(userData, processUser);
123
```
124
125
### 3. Manual Context Restoration
126
127
Use `executeAsync` for manual control:
128
129
```typescript
130
import { executeAsync } from "unctx";
131
132
const userContext = createContext<User>();
133
134
await userContext.callAsync(userData, async () => {
135
// Manual async execution with restoration
136
const [promise, restore] = executeAsync(async () => {
137
return fetch("/api/data").then(r => r.json());
138
});
139
140
const result = await promise;
141
restore(); // Manually restore context
142
143
// ✅ Context available after manual restoration
144
console.log(userContext.use());
145
});
146
```
147
148
## Concurrent Async Contexts
149
150
AsyncLocalStorage enables proper context isolation for concurrent operations:
151
152
```typescript
153
import { createContext } from "unctx";
154
import { AsyncLocalStorage } from "node:async_hooks";
155
156
const sessionContext = createContext<Session>({
157
asyncContext: true,
158
AsyncLocalStorage
159
});
160
161
// Multiple concurrent requests with isolated contexts
162
await Promise.all([
163
sessionContext.callAsync(
164
{ userId: "user1", sessionId: "sess1" },
165
async () => {
166
await new Promise(resolve => setTimeout(resolve, 100));
167
console.log(sessionContext.use().sessionId); // "sess1"
168
}
169
),
170
171
sessionContext.callAsync(
172
{ userId: "user2", sessionId: "sess2" },
173
async () => {
174
await new Promise(resolve => setTimeout(resolve, 200));
175
console.log(sessionContext.use().sessionId); // "sess2"
176
}
177
),
178
179
sessionContext.callAsync(
180
{ userId: "user3", sessionId: "sess3" },
181
async () => {
182
await new Promise(resolve => setTimeout(resolve, 50));
183
console.log(sessionContext.use().sessionId); // "sess3"
184
}
185
)
186
]);
187
```
188
189
## Error Handling in Async Context
190
191
Context is properly restored even when errors occur:
192
193
```typescript
194
const errorContext = createContext<string>({
195
asyncContext: true,
196
AsyncLocalStorage
197
});
198
199
try {
200
await errorContext.callAsync("test-value", async () => {
201
console.log(errorContext.use()); // "test-value"
202
203
await new Promise(resolve => setTimeout(resolve, 100));
204
205
throw new Error("Something went wrong");
206
});
207
} catch (error) {
208
console.error("Caught error:", error.message);
209
210
// Context is cleaned up properly
211
console.log(errorContext.tryUse()); // null
212
}
213
```
214
215
## Manual Context Restoration Patterns
216
217
### Pattern 1: Try/Catch with Restoration
218
219
```typescript
220
const ctx = createContext<Data>();
221
222
await ctx.callAsync(data, async () => {
223
const [promise, restore] = executeAsync(async () => {
224
await riskyOperation();
225
return processData();
226
});
227
228
try {
229
const result = await promise;
230
restore();
231
232
// Use context after successful operation
233
const contextData = ctx.use();
234
return processResult(result, contextData);
235
} catch (error) {
236
restore(); // Restore even on error
237
throw error;
238
}
239
});
240
```
241
242
### Pattern 2: Finally Block Restoration
243
244
```typescript
245
const ctx = createContext<Config>();
246
247
await ctx.callAsync(config, async () => {
248
let restore: (() => void) | undefined;
249
250
try {
251
const [promise, restoreFn] = executeAsync(async () => {
252
return await longRunningOperation();
253
});
254
255
restore = restoreFn;
256
const result = await promise;
257
258
// Process with restored context
259
const currentConfig = ctx.use();
260
return finalizeResult(result, currentConfig);
261
262
} finally {
263
restore?.(); // Always restore context
264
}
265
});
266
```
267
268
## Platform Compatibility
269
270
### Node.js Environment
271
272
```typescript
273
import { createContext } from "unctx";
274
import { AsyncLocalStorage } from "node:async_hooks";
275
276
const nodeContext = createContext({
277
asyncContext: true,
278
AsyncLocalStorage
279
});
280
```
281
282
### Browser/Edge Runtime
283
284
```typescript
285
// Use polyfill or build-time transformation for non-Node environments
286
import { createContext } from "unctx";
287
288
// Without AsyncLocalStorage, use build plugins
289
const browserContext = createContext();
290
291
// Requires withAsyncContext + build transformation
292
const handler = withAsyncContext(async () => {
293
await browserContext.callAsync(data, async () => {
294
// Context preserved by transformation
295
const value = browserContext.use();
296
});
297
});
298
```
299
300
### Cloudflare Workers
301
302
```typescript
303
// Cloudflare Workers provide AsyncLocalStorage
304
const workerContext = createContext({
305
asyncContext: true,
306
AsyncLocalStorage: globalThis.AsyncLocalStorage
307
});
308
```
309
310
## Performance Considerations
311
312
### AsyncLocalStorage Overhead
313
314
```typescript
315
// AsyncLocalStorage has minimal overhead for context access
316
const perfContext = createContext<PerfData>({
317
asyncContext: true,
318
AsyncLocalStorage
319
});
320
321
// Efficient: context lookups are fast
322
function highFrequencyOperation() {
323
const data = perfContext.use(); // Fast lookup
324
return processData(data);
325
}
326
```
327
328
### Transformation vs Native
329
330
```typescript
331
// Native AsyncLocalStorage (recommended for Node.js)
332
const nativeContext = createContext({
333
asyncContext: true,
334
AsyncLocalStorage
335
});
336
337
// Build-time transformation (for universal compatibility)
338
const transformedHandler = withAsyncContext(async () => {
339
// Requires bundler plugin
340
});
341
```
342
343
## Best Practices
344
345
### Choose the Right Approach
346
347
```typescript
348
// ✅ For Node.js servers: Use AsyncLocalStorage
349
const serverContext = createContext({
350
asyncContext: true,
351
AsyncLocalStorage
352
});
353
354
// ✅ For universal libraries: Use withAsyncContext + plugins
355
const universalHandler = withAsyncContext(async () => {
356
// Works everywhere with proper build setup
357
});
358
359
// ✅ For manual control: Use executeAsync
360
const [promise, restore] = executeAsync(asyncOperation);
361
```
362
363
### Context Caching
364
365
```typescript
366
// ✅ Cache context at function start for performance
367
async function processWithCaching() {
368
const ctx = myContext.use(); // Cache early
369
370
await Promise.all([
371
operation1(ctx), // Use cached value
372
operation2(ctx), // Use cached value
373
operation3(ctx) // Use cached value
374
]);
375
}
376
```