nextjs - ✅(Solved) Fix Importing next/headers in next.config.ts causes workUnitAsyncStorage invariant error in pnpm workspace monorepo [1 pull requests, 3 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#90669Fetched 2026-04-08 00:19:39
View on GitHub
Comments
3
Participants
3
Timeline
5
Reactions
0
Timeline (top)
commented ×2cross-referenced ×1issue_type_added ×1labeled ×1

Error Message

InvariantError: Invariant: Expected workUnitAsyncStorage to have a store.

Fix Action

Fixed

PR fix notes

PR #90711: fix: clear module caches after next.config.ts loading to prevent pnpm symlink conflicts

Description (problem / solution / changelog)

Summary

  • Fixes InvariantError: Expected workUnitAsyncStorage to have a store when next/headers is transitively imported during next.config.ts resolution in pnpm workspace monorepos
  • Snapshots require.cache and Module._pathCache before config loading, then removes newly added entries in the finally block so runtime module resolution starts clean

Background

In pnpm workspaces, require() during config loading resolves modules through workspace symlinks (e.g. core/node_modules/next/headers.js), populating Module._pathCache and require.cache with those symlink paths. Later at runtime, Node.js resolves the same modules to their real paths under .pnpm/, bypassing the cached entries and creating separate module instances — each with its own AsyncLocalStorage singleton. This causes getStore() to return undefined during SSR rendering.

The fix ensures that any module cache entries created during next.config.ts transpilation are cleaned up, so the runtime can resolve modules fresh via their canonical (real) paths.

How it works

  1. Before requireFromString() runs the transpiled config, snapshot the keys of require.cache and Module._pathCache
  2. After config loading completes (in the finally block, alongside deregisterHook()), delete any cache entries that were added during the process
  3. This is safe because config loading is a one-time operation — the loaded config object is already captured and returned

Test plan

  • Verified against the reproduction in #90669 (bigcommerce/catalyst monorepo with pnpm workspaces)
  • Importing next/headers transitively in next.config.ts no longer causes the invariant error
  • Non-monorepo and non-pnpm setups are unaffected since the cleanup is a no-op when symlinks are not involved

Fixes #90669

Changed files

  • AGENTS.md (modified, +4/-4)
  • packages/next/src/build/next-config-ts/transpile-config.ts (modified, +26/-0)

Code Example

git clone https://github.com/bigcommerce/catalyst.git
    cd catalyst
    git checkout CATALYST-1791-upgrade-next
    pnpm install

---

import { configClient } from "./client/config-client";

---

import { client as configClient } from "./client";

---

pnpm run dev

---

InvariantError: Invariant: Expected workUnitAsyncStorage to have a store.

---

InvariantError: Invariant: Expected workUnitAsyncStorage to have a store. This is a bug in Next.js.

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:53:05 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T6020
  Available memory (MB): 16384
  Available CPU cores: 10
Binaries:
  Node: 24.13.0
  npm: 11.6.2
  Yarn: N/A
  pnpm: 10.12.4
Relevant Packages:
  next: 16.1.6 // Latest available version is detected (16.1.6).
  eslint-config-next: 15.5.10
  react: 19.1.5
  react-dom: 19.1.5
  typescript: 5.8.3
Next.js Config:
  output: N/A
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/bigcommerce/catalyst/tree/CATALYST-1791-upgrade-next

To Reproduce

  1. Clone the repository and switch to the reproduction branch:
    git clone https://github.com/bigcommerce/catalyst.git
    cd catalyst
    git checkout CATALYST-1791-upgrade-next
    pnpm install
  2. In core/next.config.ts, change line 6 from:
    import { configClient } from "./client/config-client";
    to:
    import { client as configClient } from "./client";
    The key difference between the two imports is that core/client/index.ts has import { headers } from 'next/headers' at line 2, while core/client/config-client.ts is a duplicate that avoids that import.
  3. Run the dev server:
    pnpm run dev
  4. Visit http://localhost:3000 → 500 error with:
    InvariantError: Invariant: Expected workUnitAsyncStorage to have a store.
  5. Revert the import back to config-client → app works normally.

Current vs. Expected behavior

Current behavior:

When next/headers is imported (even transitively, even if headers() is never called) during next.config.ts resolution in a pnpm workspace monorepo, all page renders fail with:

InvariantError: Invariant: Expected workUnitAsyncStorage to have a store. This is a bug in Next.js.

This worked fine in Next.js 15.

Expected behavior:

Importing next/headers at the top level of a module used in next.config.ts should not poison the AsyncLocalStorage context for the runtime. If the import is problematic, Next.js should either handle it gracefully or surface a clear error during config resolution rather than failing silently at render time.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0: Wed Jan 28 20:53:05 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T6020
  Available memory (MB): 16384
  Available CPU cores: 10
Binaries:
  Node: 24.13.0
  npm: 11.6.2
  Yarn: N/A
  pnpm: 10.12.4
Relevant Packages:
  next: 16.1.6 // Latest available version is detected (16.1.6).
  eslint-config-next: 15.5.10
  react: 19.1.5
  react-dom: 19.1.5
  typescript: 5.8.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Module Resolution

Which stage(s) are affected? (Select all that apply)

next dev (local)

Additional context

This issue only reproduces in a pnpm workspace monorepo with a sufficiently complex dependency graph. We have been unable to reproduce it in any minimal environment:

  • A non-monorepo standalone project
  • A minimal pnpm workspace monorepo matching the same directory structure
  • Adding next-intl/plugin, middleware, instrumentation, and locale routing to match our setup
  • Matching exact pnpm version (10.12.4) and Node.js version (24.13.0)
  • Using --preserve-symlinks / --preserve-symlinks-main flags

Our debugging suggests the issue is related to Module._pathCache being populated with the symlink path (e.g., core/node_modules/next/headers.js) during next.config.ts resolution, while the runtime resolves to the real path (.pnpm/.../next/headers.js). This results in two separate AsyncLocalStorage singleton instances — one used by the config loading phase and one by the SSR runtime — so getStore() returns undefined during rendering.

This worked fine in Next.js 15 with the same monorepo structure.

extent analysis

Fix Summary

Root fix: Prevent next/headers from being imported at module‑initialisation time when the file is also required by next.config.ts.
Do this by lazy‑loading the headers API (or any other next/* server‑only helpers) inside a function, and by adding a small “safe client” wrapper that the config can import without pulling in next/headers.
Optionally hoist a single copy of next in the pnpm workspace so the same AsyncLocalStorage instance is used everywhere.


Step‑by‑Step Fix Plan

1. Create a lazy‑load wrapper for next/headers

// core/client/headers-wrapper.ts
// <-- No top‑level import of next/headers
export async function getHeaders(): Promise<ReturnType<typeof import('next

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING