Skip to content

Conversation

@ARAldhafeeri
Copy link
Contributor

@ARAldhafeeri ARAldhafeeri commented Aug 7, 2025

This PR addresses the feature request here #6870

Problem Statement

Currently, integrating AsyncLocalStorage with tRPC requires:

  1. Manual context passing
  2. Global procedure middleware wrapping

This creates unnecessary boilerplate for a common need.

Solution

Add optional asyncStorage to runtime config that automatically wraps all procedures to improve developer experience.

Why Runtime Configuration?

  • Architectural Fit: Async context is a runtime concern, not request context
  • Backward Compatible: Optional parameter maintains existing behavior
  • Clean DX: Single configuration point vs middleware wiring

Implementation

Propagation Path

  1. Run time config → Root config → Procedure builder → Resolver → Procedure caller
  2. Preserves full type safety throughout
  3. inject context into the storage for type safety

Key Considerations

Performance

  • Minimal overhead (AsyncLocalStorage is optimized for this)
  • Only active when configured
  • short-circuit pattern run the async storage on the same index as middleware chain

Advanced Use Cases

  • Nested Procedures: Automatically inherit storage
  • Overrides: Still possible via manual .use() wrapping with not passing asyncStorage as runtime configuration
  • Middleware: Runs within storage context

Usage Example

const storage = new AsyncLocalStorage();
const t = initTRPC.create({
  asyncStorage: storage 
});

// Any procedure automatically has context storage
const router = t.router({
  getUser: t.procedure.query(() => {
    deepFunction(); // Can access context via storage.getStore()
  })
});

Alternatives Considered

  1. Context-Based: Rejected - wrong abstraction layer
  2. Plugin System: Overkill - focused use case
  3. Global Middleware: existing solution before this with .use on a procedure

Final note :

  • I have also updated the documentation and added example for this, as new concept in docs docs/server/async-context-management.md

✅ Checklist

  • [ x] I have followed the steps listed in the Contributing guide.
  • [x ] If necessary, I have added documentation related to the changes made.
  • [x ] I have added or updated the tests related to the changes made.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Added support for integrating AsyncLocalStorage for automatic context propagation across all procedures.
  • Documentation
    • Introduced new documentation explaining async context management and usage of AsyncLocalStorage with procedures.
  • Tests
    • Added tests demonstrating AsyncLocalStorage integration with minimal configuration.
  • Chores
    • Improved test setup for generating self-signed TLS certificates for better cross-platform compatibility.

ARAldhafeeri and others added 6 commits July 5, 2025 11:08
…d cross-platform valid absoulte path string with fileURLToPath, and path.resolve, running tests fails on windows without this modification
…can enable async context with one line as optional run time configuration option
@ARAldhafeeri ARAldhafeeri requested review from a team as code owners August 7, 2025 00:36
@vercel
Copy link

vercel bot commented Aug 7, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
www ❌ Failed (Inspect) Aug 7, 2025 0:51am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 7, 2025

Walkthrough

This update adds native support for Node.js AsyncLocalStorage to the tRPC server core. It introduces an optional asyncStorage property in configuration interfaces and propagates it through the server initialization and procedure builder, enabling context propagation across asynchronous calls. New tests and documentation demonstrate and explain the feature.

Changes

Cohort / File(s) Change Summary
AsyncLocalStorage Integration in Core
packages/server/src/unstable-core-do-not-import/initTRPC.ts, packages/server/src/unstable-core-do-not-import/procedureBuilder.ts, packages/server/src/unstable-core-do-not-import/rootConfig.ts
Adds optional asyncStorage property to core config and builder types; propagates and applies AsyncLocalStorage for context management in procedure execution.
Testing AsyncLocalStorage Functionality
packages/tests/server/validators.test.ts
Adds a new test verifying AsyncLocalStorage integration across procedures with minimal configuration, including context propagation and validation.
Test Infrastructure Update
packages/tests/server/adapters/standalone.http2.test.ts
Updates TLS certificate generation to use execFileSync with argument array for improved cross-platform compatibility.
Documentation Addition
www/docs/server/async-context-management.md
Introduces new documentation explaining AsyncLocalStorage integration, setup, and usage examples with tRPC server.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant tRPC as tRPC Server
    participant ALS as AsyncLocalStorage
    participant Proc as Procedure

    App->>tRPC: Initialize with asyncStorage
    tRPC->>ALS: Store context (run)
    ALS->>Proc: Execute procedure with context
    Proc->>ALS: Access context anywhere in async call chain
    Proc-->>tRPC: Return result
    tRPC-->>App: Respond with result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–25 minutes

Possibly related issues

Suggested reviewers

  • juliusmarminge

Poem

In tunnels of async, context would stray,
But now, with AsyncLocalStorage, it finds its way.
Through tRPC’s burrows, the data flows true,
No more lost carrots, just context for you!
With docs and tests, this burrow is bright—
Async context hops on, from morning till night! 🥕✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ced26b2 and aaef4ba.

📒 Files selected for processing (1)
  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.
📚 Learning: in trpc codebase, both `input` and `errors` properties consistently use `void` as their default type...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: packages/tests/server/websockets.test.ts:1962-1962
Timestamp: 2025-06-29T21:21:01.631Z
Learning: In tRPC codebase, both `input` and `errors` properties consistently use `void` as their default type value in procedure definitions, following the pattern `DefaultValue<TInputIn, void>` and `DefaultValue<TErrors, void>`. This is an established architectural decision that maintains consistency across the type system, even though it diverges from general TypeScript conventions that prefer `undefined` for property types.

Applied to files:

  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
📚 Learning: the cli options `trpcfile` and `trpcimportname` in `packages/upgrade/src/bin/cli.ts` are temporary a...
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.

Applied to files:

  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
🧬 Code Graph Analysis (1)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
packages/server/src/unstable-core-do-not-import/middleware.ts (1)
  • MiddlewareResult (36-38)
🪛 ESLint
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts

[error] 647-647: Forbidden non-null assertion.

(@typescript-eslint/no-non-null-assertion)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Lint and auto-fix
  • GitHub Check: test
  • GitHub Check: Analyze (typescript)
🔇 Additional comments (2)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (2)

1-1: LGTM - Clean import addition.

The import of AsyncLocalStorage from Node.js async_hooks is appropriately typed and follows the established import patterns in the file.


91-95: LGTM - Well-documented type addition.

The optional asyncStorage property is properly typed and documented. The AsyncLocalStorage<any> type provides the necessary flexibility for various context types while maintaining type safety.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel
Copy link

vercel bot commented Aug 7, 2025

@ARAldhafeeri is attempting to deploy a commit to the trpc Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/server/src/unstable-core-do-not-import/rootConfig.ts (1)

1-1: Fix import type to comply with ESLint rules.

The AsyncLocalStorage import is only used as a type in this file, so it should be imported as a type.

Apply this diff to fix the ESLint issue:

-import { AsyncLocalStorage } from 'async_hooks';
+import type { AsyncLocalStorage } from 'async_hooks';
packages/server/src/unstable-core-do-not-import/initTRPC.ts (1)

1-1: Fix import type to comply with ESLint rules.

Similar to the rootConfig file, AsyncLocalStorage is only used as a type here.

Apply this diff to fix the ESLint issue:

-import { AsyncLocalStorage } from 'async_hooks';
+import type { AsyncLocalStorage } from 'async_hooks';
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)

1-1: Fix import type to comply with ESLint rules.

Consistent with other files, AsyncLocalStorage should be imported as a type.

Apply this diff to fix the ESLint issue:

-import { AsyncLocalStorage } from 'async_hooks';
+import type { AsyncLocalStorage } from 'async_hooks';
packages/tests/server/validators.test.ts (1)

719-727: Consider adding more descriptive error message.

The error handling is correct, but the error message could be more specific to help with debugging.

-      throw new Error('No context found');
+      throw new Error('AsyncLocalStorage context not found - ensure procedure is called within async storage scope');
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c93a1f and 722c3ea.

📒 Files selected for processing (6)
  • packages/server/src/unstable-core-do-not-import/initTRPC.ts (5 hunks)
  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (3 hunks)
  • packages/server/src/unstable-core-do-not-import/rootConfig.ts (2 hunks)
  • packages/tests/server/adapters/standalone.http2.test.ts (1 hunks)
  • packages/tests/server/validators.test.ts (1 hunks)
  • www/docs/server/async-context-management.md (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.
📚 Learning: the cli options `trpcfile` and `trpcimportname` in `packages/upgrade/src/bin/cli.ts` are temporary a...
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.

Applied to files:

  • packages/server/src/unstable-core-do-not-import/initTRPC.ts
  • www/docs/server/async-context-management.md
  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
  • packages/server/src/unstable-core-do-not-import/rootConfig.ts
📚 Learning: in the trpc monorepo, packages/tests and packages/upgrade don't need main/module/types fields in the...
Learnt from: juliusmarminge
PR: trpc/trpc#6789
File: packages/tanstack-react-query/tsdown.config.ts:1-24
Timestamp: 2025-06-09T14:01:20.033Z
Learning: In the tRPC monorepo, packages/tests and packages/upgrade don't need main/module/types fields in their package.json because tests is not distributed and upgrade just exposes a binary.

Applied to files:

  • packages/server/src/unstable-core-do-not-import/initTRPC.ts
📚 Learning: in trpc codebase, both `input` and `errors` properties consistently use `void` as their default type...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: packages/tests/server/websockets.test.ts:1962-1962
Timestamp: 2025-06-29T21:21:01.631Z
Learning: In tRPC codebase, both `input` and `errors` properties consistently use `void` as their default type value in procedure definitions, following the pattern `DefaultValue<TInputIn, void>` and `DefaultValue<TErrors, void>`. This is an established architectural decision that maintains consistency across the type system, even though it diverges from general TypeScript conventions that prefer `undefined` for property types.

Applied to files:

  • packages/server/src/unstable-core-do-not-import/initTRPC.ts
  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
📚 Learning: in the minimal-react-typesafe-errors example, the database mock in db.ts should throw generic error ...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: examples/minimal-react-typesafe-errors/server/db.ts:18-20
Timestamp: 2025-06-29T21:25:46.435Z
Learning: In the minimal-react-typesafe-errors example, the database mock in db.ts should throw generic Error objects rather than typed errors. The typed errors are handled at the tRPC procedure layer, which catches database errors and transforms them into API-specific typed errors. This maintains proper architectural separation between the database layer and API layer.

Applied to files:

  • packages/tests/server/validators.test.ts
📚 Learning: in the minimal-typesafe-errors example, the database mock in db.ts should throw generic error object...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: examples/minimal-typesafe-errors/src/server/db.ts:18-20
Timestamp: 2025-06-29T21:26:35.189Z
Learning: In the minimal-typesafe-errors example, the database mock in db.ts should throw generic Error objects rather than typed errors. The typed errors are handled at the tRPC procedure layer, which catches database errors and transforms them into API-specific typed errors. This maintains proper architectural separation between the database layer and API layer.

Applied to files:

  • packages/tests/server/validators.test.ts
📚 Learning: in the minimal-react-typesafe-errors example, the `trpc.user.list` query is designed to never throw ...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: examples/minimal-react-typesafe-errors/client/src/Users.tsx:6-6
Timestamp: 2025-06-29T21:21:07.306Z
Learning: In the minimal-react-typesafe-errors example, the `trpc.user.list` query is designed to never throw errors since it's a minimal example focused on demonstrating typed error handling in mutations rather than queries.

Applied to files:

  • packages/tests/server/validators.test.ts
🧬 Code Graph Analysis (2)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
packages/server/src/unstable-core-do-not-import/middleware.ts (1)
  • MiddlewareResult (36-38)
packages/tests/server/validators.test.ts (4)
packages/server/src/unstable-core-do-not-import/initTRPC.ts (1)
  • initTRPC (233-233)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
  • input (503-509)
packages/tests/server/___testHelpers.ts (1)
  • routerToServerAndClientNew (36-74)
packages/server/src/__tests__/waitError.ts (1)
  • waitError (3-40)
🪛 ESLint
packages/server/src/unstable-core-do-not-import/initTRPC.ts

[error] 1-1: All imports in the declaration are only used as types. Use import type.

(@typescript-eslint/consistent-type-imports)

packages/server/src/unstable-core-do-not-import/procedureBuilder.ts

[error] 1-1: All imports in the declaration are only used as types. Use import type.

(@typescript-eslint/consistent-type-imports)


[error] 647-647: Forbidden non-null assertion.

(@typescript-eslint/no-non-null-assertion)

packages/server/src/unstable-core-do-not-import/rootConfig.ts

[error] 1-1: All imports in the declaration are only used as types. Use import type.

(@typescript-eslint/consistent-type-imports)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Lint and auto-fix
  • GitHub Check: build
  • GitHub Check: test
  • GitHub Check: Analyze (typescript)
🔇 Additional comments (11)
packages/tests/server/adapters/standalone.http2.test.ts (1)

26-49: Excellent security and cross-platform improvement!

The refactoring from execSync with concatenated string to execFileSync with structured arguments prevents shell injection vulnerabilities and improves cross-platform compatibility. The windowsHide: true option is a thoughtful addition for Windows users.

packages/server/src/unstable-core-do-not-import/rootConfig.ts (1)

91-94: LGTM! Clean integration of AsyncLocalStorage into root config.

The optional asyncStorage property is properly typed and documented. Using AsyncLocalStorage<any> provides the necessary flexibility for different context types while maintaining type safety.

packages/server/src/unstable-core-do-not-import/initTRPC.ts (3)

52-55: Well-documented configuration option.

The asyncStorage property is properly integrated into the runtime configuration with clear documentation.


174-177: LGTM! Proper propagation to internal config.

The asyncStorage option is correctly passed through to the internal RootConfig with appropriate comments.


203-203: Correct forwarding to procedure builder.

The asyncStorage option is properly passed to the procedure builder, completing the configuration propagation chain.

packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (2)

91-94: LGTM! Proper integration into procedure builder definition.

The asyncStorage property is correctly added to the ProcedureBuilderDef type with appropriate documentation.


632-648: Excellent AsyncLocalStorage implementation pattern!

The implementation correctly:

  • Wraps execution only at the first middleware call (index === 0)
  • Prevents infinite recursion by clearing asyncStorage in the recursive call
  • Uses the proper asyncStorage.run(context, callback) pattern
  • Stores the result outside the run callback to return it properly

The non-null assertion on Line 647 is safe because the result is guaranteed to be set within the async callback.

However, you can address the ESLint warning by initializing the variable:

-      let result: MiddlewareResult<any>;
+      let result: MiddlewareResult<any> | undefined;
       
       await _def.asyncStorage.run(opts.ctx, async () => {
         // Process the middleware chain without asyncStorage to prevent infinite recursion
         result = await callRecursive(
           0,
           { ..._def, asyncStorage: undefined },
           opts,
         );
       });
       
       // Return the result from the async context
-      return result!;
+      return result as MiddlewareResult<any>;
packages/tests/server/validators.test.ts (4)

708-717: LGTM! Clean demonstration of the new AsyncLocalStorage feature.

The test effectively demonstrates the new asyncStorage configuration option that reduces boilerplate compared to manual middleware setup. The type definition and tRPC initialization are correct.


733-739: Excellent demonstration of context usage in input parser.

This shows the key benefit of the feature - the input parser can directly access the context via AsyncLocalStorage without requiring additional middleware setup. The assertion validates that the context is properly propagated.


746-752: Good test setup with context provider.

The server configuration correctly provides the expected context that will be injected into AsyncLocalStorage.


753-776: Comprehensive test coverage with proper error validation.

The test covers both the success case and validation error case with appropriate snapshots. The error message format matches the expected zod v3 validation error structure.

Comment on lines 1 to 43
## Async Context Management

tRPC's `AsyncLocalStorage` integration automatically propagates context through async calls.

### Setup

```ts
import { AsyncLocalStorage } from 'async_hooks';

const t = initTRPC.context<{ user: { id: string } }>().create({
asyncStorage: new AsyncLocalStorage(), // Enable auto-context
});
```

### Key Features

1. **Access context anywhere**

```ts
const router = t.router({
getUser: t.procedure.query(() => {
const ctx = t.asyncStorage.getStore(); // No prop drilling
return ctx?.user;
})
});
```

2. **Works with nested calls**

```ts

const router = t.router({
getUser: t.procedure.query(() => {
const users = await getUserFromDB();
})
});

async function getUserFromDB() {
// Still has access to original context
const { user } = t.asyncStorage.getStore()!;
return db.users.find(user.id);
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the API usage in documentation examples.

The documentation is well-structured and covers the key features, but I need to verify that the API usage (t.asyncStorage.getStore()) matches the actual implementation.

Let me check if the t.asyncStorage property exists and how users should access the AsyncLocalStorage instance:


🏁 Script executed:

#!/bin/bash
# Search for asyncStorage usage patterns in the codebase
echo "Searching for asyncStorage access patterns..."
rg -A 3 -B 3 "asyncStorage\.getStore|\.asyncStorage" --type ts

echo "Searching for AsyncLocalStorage property definitions..."
rg -A 5 -B 2 "asyncStorage.*:" --type ts

echo "Searching for procedure builder usage with asyncStorage..."
rg -A 10 -B 5 "createBuilder.*asyncStorage" --type ts

Length of output: 7644


I couldn’t find how the public t object exposes the asyncStorage instance. Let me pull the relevant creation logic to confirm whether t.asyncStorage exists or if a different accessor is required:


🏁 Script executed:

#!/bin/bash
# Show the context builder and create() return structure
rg -n "export function context" -A 20 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.ts

# Show where create() is defined and what it returns
rg -n ".create(" -A 10 -B 10 packages/server/src/unstable-core-do-not-import/initTRPC.ts

Length of output: 262


I didn’t catch the initTRPC builder or its .create() signature. Let’s re-run with escaped regexes to pull in the full definition and return shape:


🏁 Script executed:

#!/bin/bash
echo "Finding initTRPC definition and options..."
rg -n "function initTRPC" -A 20 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.ts

echo "Inspecting context builder call site..."
rg -n "\.context<" -A 10 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.ts

echo "Locating the .create\(" -A 15 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.ts

Length of output: 571


I still can’t locate the public t shape or the .create() return type to confirm if asyncStorage is exposed. Let me dump the top of the initTRPC.ts file to inspect how the context builder and .create() are implemented:


🏁 Script executed:

#!/bin/bash
# Dump the first 200 lines of the initTRPC implementation
sed -n '1,200p' packages/server/src/unstable-core-do-not-import/initTRPC.ts

Length of output: 5799


Documentation: Expose and use the AsyncLocalStorage instance, not t.asyncStorage

The public TRPCRootObject does not include an asyncStorage property—asyncStorage is only kept internally on the builder’s config. Users must keep a reference to the AsyncLocalStorage they pass into .create() and call its methods directly.

Suggested updates:

  • In “Setup,” capture the storage instance:

    import { AsyncLocalStorage } from 'async_hooks';
    
    const asyncStorage = new AsyncLocalStorage<{ user: { id: string } }>();
    
    const t = initTRPC
      .context<{ user: { id: string } }>()
      .create({ asyncStorage });
  • In examples, replace t.asyncStorage.getStore() with asyncStorage.getStore() (and similarly for .run()):

    - const ctx = t.asyncStorage.getStore();
    + const ctx = asyncStorage.getStore();
    - await t.asyncStorage.run(opts.ctx, async () => { … });
    + await asyncStorage.run(opts.ctx, async () => { … });

Please update all occurrences accordingly so code samples compile against the public API.

🤖 Prompt for AI Agents
In www/docs/server/async-context-management.md lines 1 to 43, the code examples
incorrectly use t.asyncStorage to access AsyncLocalStorage, but the public
TRPCRootObject does not expose asyncStorage. To fix this, declare and export the
AsyncLocalStorage instance separately during setup, assign it to a variable
(e.g., asyncStorage), pass it into t.create(), and update all code samples to
use asyncStorage.getStore() and asyncStorage.run() instead of t.asyncStorage
methods to align with the public API.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Aug 7, 2025

Open in StackBlitz

@trpc/client

npm i https://pkg.pr.new/trpc/trpc/@trpc/client@6881

@trpc/next

npm i https://pkg.pr.new/trpc/trpc/@trpc/next@6881

@trpc/react-query

npm i https://pkg.pr.new/trpc/trpc/@trpc/react-query@6881

@trpc/server

npm i https://pkg.pr.new/trpc/trpc/@trpc/server@6881

@trpc/tanstack-react-query

npm i https://pkg.pr.new/trpc/trpc/@trpc/tanstack-react-query@6881

@trpc/upgrade

npm i https://pkg.pr.new/trpc/trpc/@trpc/upgrade@6881

commit: aaef4ba

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 722c3ea and ced26b2.

📒 Files selected for processing (4)
  • packages/server/src/unstable-core-do-not-import/initTRPC.ts (5 hunks)
  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (3 hunks)
  • packages/server/src/unstable-core-do-not-import/rootConfig.ts (2 hunks)
  • www/docs/server/async-context-management.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • www/docs/server/async-context-management.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/server/src/unstable-core-do-not-import/rootConfig.ts
  • packages/server/src/unstable-core-do-not-import/initTRPC.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.
📚 Learning: in trpc codebase, both `input` and `errors` properties consistently use `void` as their default type...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: packages/tests/server/websockets.test.ts:1962-1962
Timestamp: 2025-06-29T21:21:01.631Z
Learning: In tRPC codebase, both `input` and `errors` properties consistently use `void` as their default type value in procedure definitions, following the pattern `DefaultValue<TInputIn, void>` and `DefaultValue<TErrors, void>`. This is an established architectural decision that maintains consistency across the type system, even though it diverges from general TypeScript conventions that prefer `undefined` for property types.

Applied to files:

  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
📚 Learning: the cli options `trpcfile` and `trpcimportname` in `packages/upgrade/src/bin/cli.ts` are temporary a...
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.

Applied to files:

  • packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
🧬 Code Graph Analysis (1)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
packages/server/src/unstable-core-do-not-import/middleware.ts (1)
  • MiddlewareResult (36-38)
🪛 ESLint
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts

[error] 647-647: Forbidden non-null assertion.

(@typescript-eslint/no-non-null-assertion)

🪛 GitHub Check: Lint and auto-fix
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts

[failure] 647-647:
Forbidden non-null assertion

🪛 GitHub Actions: autofix.ci
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts

[error] 647-647: ESLint: Forbidden non-null assertion (@typescript-eslint/no-non-null-assertion)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: E2E-tests (next-prisma-websockets-starter)
  • GitHub Check: E2E-tests (express-minimal)
  • GitHub Check: E2E-tests (next-minimal-starter)
  • GitHub Check: E2E-tests (minimal-react)
  • GitHub Check: E2E-tests (.test/ssg)
  • GitHub Check: E2E-tests (express-server)
  • GitHub Check: E2E-tests (cloudflare-workers)
  • GitHub Check: E2E-tests (fastify-server)
  • GitHub Check: E2E-tests (.test/internal-types-export)
  • GitHub Check: E2E-tests (.experimental/next-app-dir)
  • GitHub Check: e2e-legacy-node (next-prisma-websockets-starter, 18.x)
  • GitHub Check: e2e-legacy-node (next-prisma-starter, 18.x)
  • GitHub Check: e2e-legacy-node (next-prisma-websockets-starter, 20.x)
  • GitHub Check: e2e-legacy-node (next-prisma-todomvc, 18.x)
  • GitHub Check: e2e-legacy-node (next-prisma-starter, 20.x)
  • GitHub Check: Release using pkg.pr.new
  • GitHub Check: E2E-tests (Bun) (bun, ubuntu-latest)
  • GitHub Check: E2E-tests (Deno) (deno-deploy)
  • GitHub Check: typecheck
  • GitHub Check: test
🔇 Additional comments (2)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (2)

1-1: LGTM!

Correct type-only import of AsyncLocalStorage from Node.js async_hooks module.


91-94: LGTM!

Well-documented optional property that maintains backward compatibility. The AsyncLocalStorage<any> typing is appropriate for flexible context storage.

Comment on lines 632 to 648
// If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
if (index === 0 && _def.asyncStorage) {
// Store the result outside the run() callback
let result: MiddlewareResult<any>;

await _def.asyncStorage.run(opts.ctx, async () => {
// Process the middleware chain without asyncStorage to prevent هnfinite recursion
result = await callRecursive(
0,
{ ..._def, asyncStorage: undefined },
opts,
);
});

// Return the result from the async context
return result!;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix non-null assertion and typo in AsyncLocalStorage implementation.

The AsyncLocalStorage integration logic is sound, but there are a few issues to address:

  1. ESLint Error: The non-null assertion on line 647 is flagged by linting rules and could be unsafe
  2. Typo: Line 638 contains an Arabic character ("هnfinite") that should be "infinite"
  3. Potential Safety Issue: While the logic should always set result, there's no explicit guarantee

Apply this diff to fix these issues:

    // If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
    if (index === 0 && _def.asyncStorage) {
      // Store the result outside the run() callback
      let result: MiddlewareResult<any>;

      await _def.asyncStorage.run(opts.ctx, async () => {
-        // Process the middleware chain without asyncStorage to prevent هnfinite recursion
+        // Process the middleware chain without asyncStorage to prevent infinite recursion
        result = await callRecursive(
          0,
          { ..._def, asyncStorage: undefined },
          opts,
        );
      });

      // Return the result from the async context
-      return result!;
+      return result;
    }

The non-null assertion can be safely removed because the await ensures the callback completes before returning, and the recursive call will always produce a result (either success or error).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
if (index === 0 && _def.asyncStorage) {
// Store the result outside the run() callback
let result: MiddlewareResult<any>;
await _def.asyncStorage.run(opts.ctx, async () => {
// Process the middleware chain without asyncStorage to prevent هnfinite recursion
result = await callRecursive(
0,
{ ..._def, asyncStorage: undefined },
opts,
);
});
// Return the result from the async context
return result!;
}
// If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
if (index === 0 && _def.asyncStorage) {
// Store the result outside the run() callback
let result: MiddlewareResult<any>;
await _def.asyncStorage.run(opts.ctx, async () => {
// Process the middleware chain without asyncStorage to prevent infinite recursion
result = await callRecursive(
0,
{ ..._def, asyncStorage: undefined },
opts,
);
});
// Return the result from the async context
return result;
}
🧰 Tools
🪛 ESLint

[error] 647-647: Forbidden non-null assertion.

(@typescript-eslint/no-non-null-assertion)

🪛 GitHub Check: Lint and auto-fix

[failure] 647-647:
Forbidden non-null assertion

🪛 GitHub Actions: autofix.ci

[error] 647-647: ESLint: Forbidden non-null assertion (@typescript-eslint/no-non-null-assertion)

🤖 Prompt for AI Agents
In packages/server/src/unstable-core-do-not-import/procedureBuilder.ts around
lines 632 to 648, remove the non-null assertion on the returned result variable
to fix the ESLint error and improve safety, correct the typo from the Arabic
character "ه" to "i" in the comment to fix the word "infinite," and ensure the
code logic clearly guarantees that the result is always assigned before
returning by relying on the await to complete the asyncStorage.run callback.

@ARAldhafeeri
Copy link
Contributor Author

Hello, any updates on this ?

@@ -1,3 +1,4 @@
import type { AsyncLocalStorage } from 'async_hooks';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't import from Node, instead we should use a duck type that looks like AsyncLocalStorage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with an interface like

interface AsyncStorageLike<T> {
  run<TResult>(store: T, fn: (store: T) => Promise<TResult>): Promise<TResult>;
}

Comment on lines +632 to +648
// If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
if (index === 0 && _def.asyncStorage) {
// Store the result outside the run() callback
let result: MiddlewareResult<any>;

await _def.asyncStorage.run(opts.ctx, async () => {
// Process the middleware chain without asyncStorage to prevent infinite recursion
result = await callRecursive(
0,
{ ..._def, asyncStorage: undefined },
opts,
);
});

// Return the result from the async context
return result!;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
if (index === 0 && _def.asyncStorage) {
// Store the result outside the run() callback
let result: MiddlewareResult<any>;
await _def.asyncStorage.run(opts.ctx, async () => {
// Process the middleware chain without asyncStorage to prevent infinite recursion
result = await callRecursive(
0,
{ ..._def, asyncStorage: undefined },
opts,
);
});
// Return the result from the async context
return result!;
}
// If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
if (index === 0 && _def.asyncStorage) {
return await _def.asyncStorage.run(opts.ctx, async () => {
// Process the middleware chain without asyncStorage to prevent infinite recursion
return await callRecursive(
0,
{ ..._def, asyncStorage: undefined },
opts,
);
});
}


const t = initTRPC.context<Context>().create({
asyncStorage: contextStorage,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, with the types you've put in, it always has to be a context storage - I think it'd be nice to separate the context from the data in the store so your async storage can have a type that doesn't match the context..

type Context = {
  foo: string;
};

const contextStorage = new AsyncLocalStorage<{
  something: string;
  ctx: Context;
}>();

const t = initTRPC.context<Context>().create({
  asyncStorage: {
    storage: contextStorage,
    // called whenever a procedure is called
    getStore: async (opts) => {
      return {
        ctx: opts.ctx,
        something: 'else',
      };
    },
  },
});

Copy link
Contributor Author

@ARAldhafeeri ARAldhafeeri Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello 👋,

What do you think about simply using:

AsyncLocalStorage<unknown>

That way developers can cast to their own type when needed. Since ALS is strictly server-side, type safety across the client–server boundary doesn’t add much practical value, it just introduces extra boilerplate.

On the breaking getStore change: I see the biggest win in keeping AsyncLocalStorage integration a one-line config. Developers can define their async local storage (with any helper methods they want) in one place and export only the storage. Inside procedures, they can call getStore when needed.

Not every procedure needs async local storage, but to make one-line config possible, we currently run storage on all procedures. The overhead of running storage each time is minimal, and developers can still wrap their own helpers around it if desired.

Your call though, if you’d prefer me to implement this differently as you instructed, let me know. You’re the creator and know best.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Katt,
Any update on this ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants