or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async-context.mdbuild-plugins.mdcontext-creation.mdindex.mdnamespace-management.md

async-context.mddocs/

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

```