-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(nextjs): Add ALS runner fallbacks for serverless environments #18889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(nextjs): Add ALS runner fallbacks for serverless environments #18889
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds fallback logic for AsyncLocalStorage (ALS) snapshots in serverless environments like Cloudflare Workers. The issue occurs when ALS snapshots become stale after the request context in which they were created ends, causing "Cannot call this AsyncLocalStorage bound function outside of the request" errors.
Changes:
- Replaced direct snapshot assignment with a wrapper function that implements retry logic with fallback
- Added helper functions
getAsyncLocalStorageSnapshot()andisAsyncLocalStorageError()to modularize snapshot creation and error detection - Renamed type alias from
_INTERNAL_RandomSafeContextRunnertoRandomSafeContextRunnerfor cleaner local naming
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| function isAsyncLocalStorageError(error: unknown): boolean { | ||
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error detection logic is too broad and may incorrectly catch errors that merely mention "AsyncLocalStorage" in their message but aren't the specific ALS binding error that needs to be handled. Consider checking for the exact error message: "Cannot call this AsyncLocalStorage bound function outside of the request in which it was created." This will prevent false positives where unrelated errors that happen to mention AsyncLocalStorage in their message would incorrectly trigger the retry/fallback logic.
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); | |
| return ( | |
| error instanceof Error && | |
| error.message === | |
| 'Cannot call this AsyncLocalStorage bound function outside of the request in which it was created.' | |
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exact error matching is flimsy IMO, also I am not sure if it would be the exact same error in each serverless environment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also not sure what would be the best call here, I think just AsyncLocalStorage is fine when thrown at this point
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
chargome
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for updating! I guess there is no way to properly reflect this in a e2e test so lgtm!
| throw error; | ||
| } | ||
|
|
||
| // Snapshot likely stale, try to get a fresh one and retry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can the snapshot get stale?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it's not the best way to describe this, but this was the closest analogy. This is what I found while debugging, when we set the ALS snapshot on startup:
- if cold start it gets initialised again and it works as expected, at that point the snapshot isn't set, so the callbacks execute normally.
- if warm start, the global already exists, so some random APIs might run before the
inittakes effect. It will run with the previous snapshot that was set which would be in a different request context.
Or that's what I think is happening.
| } | ||
|
|
||
| function isAsyncLocalStorageError(error: unknown): boolean { | ||
| return error instanceof Error && error.message.includes('AsyncLocalStorage'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also not sure what would be the best call here, I think just AsyncLocalStorage is fine when thrown at this point
Summary
This was introduced by cache components workaround in #18700.
When deploying a Next.js app on Cloudflare Workers and I presume other serverless environments, you would get this error
This happens because the snapshot becomes stale in the next request invocation.
Solution
This is more of a best-effort workaround. I added a retry logic based on some criteria, if the first attempt fails due to ALS error then it will grab a new snapshot and re-run it, if that also fails, then it will run the callback directly.
the flow goes like this:
flowchart TD A[callback invoked] --> B{Try cached snapshot} B -->|Success| C[Return result] B -->|Error| D{Is AsyncLocalStorage error?} D -->|No| E[Rethrow error] D -->|Yes| F[Get fresh snapshot] F --> G{Fresh snapshot available?} G -->|No| H[Execute callback directly] G -->|Yes| I[Update cached snapshot] I --> J{Try fresh snapshot} J -->|Success| K[Return result] J -->|Error| L{Is AsyncLocalStorage error?} L -->|No| M[Rethrow error] L -->|Yes| N[Execute callback directly] H --> O[Return result] N --> O style C fill:#90EE90 style K fill:#90EE90 style O fill:#90EE90 style E fill:#FFB6C1 style M fill:#FFB6C1Closes #18842