You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Turbopack: don't crash the dev server when node_modules/next is briefly unresolvable (vercel#93877)
### What?
When `node_modules/next` is briefly unresolvable mid-session (for
example because pnpm is mid-install of a package with a `next` peer
dependency, reshuffling its symlinks) Turbopack would:
- Emit a `MissingNextFolderIssue` with severity `Fatal`
- Propagate `anyhow!("Next.js package not found")` through ~10 levels of
the build graph
- Surface the failure at the napi boundary as a
`TurbopackInternalError`, which the Next.js JS-side treats as fatal and
shuts the dev server down
The reported user impact was a dev session that became wedged and could
not recover even after restarting `next dev`, because the persistent
cache had stored the failed operation state. Users reported it as
"stuck", "catastrophic", and required wiping `.next/` to recover.
Unfortunately while i was able to reproduce a number of bad outcomes, i
couldn't actually reproduce the issue of `next dev` getting stuck in a
bad state. Still this PR will improve a number of apis and error
messages
See internal discussion:
https://vercel.slack.com/archives/C046HAU4H7F/p1778792876550309
### Why?
A transient filesystem race during an install is the canonical
recoverable error — once the install finishes, `next` is resolvable
again. There is no reason this should bring down the dev server or
poison the persistent cache.
The bug had two contributing causes:
1. The error message was poor. It assumed the only cause was a mis-set
`turbopack.root`, with the entire message in `title()`. It didn't
acknowledge the install-race case or any of the other realistic causes
(broken symlink, monorepo hoisting, removed `node_modules/next`, global
install). Users had no way to know that the right response was to wait
or refresh.
2. The Rust→napi boundary did not treat this as a recoverable error. Two
specific code paths propagated `Err` instead of catching it:
- `issue_filter_from_endpoint` in `endpoint.rs` calls
`endpoint_op.connect().await?` — if the upstream endpoint resolution
errors (as it does when `directory_tree_to_loader_tree` fails), this `?`
propagates **before** the surrounding
`strongly_consistent_catch_collectables` gets a chance to convert the
failure into Issues.
- `Project::hmr_version_state` is called bare from `project_hmr_events`
at the napi boundary. There is no `_with_issues_operation` wrapper, so
any error from the underlying
`hmr_content`/`versioned_content_map.compute_entry` chain propagates
straight to JS.
### How?
Three changes:
**1. Rewrite `MissingNextFolderIssue`**
([next_import_map.rs](crates/next-core/src/next_import_map.rs))
- Severity downgraded from `Fatal` to `Error`. `Fatal` triggers an
explicit JS-side server shutdown in `print-build-errors.ts`; `Error`
lets the dev server recover.
- Title is a single line: `"Could not find the Next.js package
(next/package.json)"`.
- Description lists all realistic causes (concurrent install,
missing/broken symlink, mis-set workspace root, monorepo hoisting,
global install).
- Doc link moved to `documentation_link()` so it doesn't pollute the
inline message.
**2. Make `issue_filter_from_endpoint` Err-tolerant**
([endpoint.rs](crates/next-napi-bindings/src/next_api/endpoint.rs))
- Wrap the `endpoint_op.connect().await` in a `match` and fall back to
`IssueFilter::warnings_and_foreign_errors()` on `Err`.
- This unblocks the existing `strongly_consistent_catch_collectables`
recovery path, which then absorbs the upstream failure as Issues that
surface through the napi boundary normally.
**3. Add `hmr_version_state_with_issues_operation`**
([project.rs](crates/next-napi-bindings/src/next_api/project.rs))
- Mirrors the existing `hmr_update_with_issues_operation` pattern.
- Drives `hmr_version_operation` as an `OperationVc`, catches any `Err`
and falls back to `NotFoundVersion`, and collects issues from the
operation via `peek_issues`.
- The napi `project_hmr_events` handler now uses this wrapper instead of
calling `Project::hmr_version_state` bare. Issues from both
`hmr_version_state` and `hmr_update` are merged before being sent to JS.
- Required lifting the inner `hmr_version_operation` out of
`Project::hmr_version_state`'s closure to make it callable from the napi
layer.
A new regression test at
[test/development/app-dir/concurrent-install/](test/development/app-dir/concurrent-install/)
reproduces the failure by moving `node_modules/next` aside
mid-HMR-session and asserts that the dev server does not emit `FATAL: An
unexpected Turbopack error occurred` or `TurbopackInternalError`, and
that the friendly Issue text surfaces. The test is skipped under
`NEXT_SKIP_ISOLATE=1` since that mode doesn't produce a manipulable
`node_modules`.
Out of scope: after recovery, the dev server still returns 500 on
subsequent requests until the user manually refreshes (the JS-side
per-route error state isn't invalidated by Turbopack's successful
recompile). That's a separate dev-server caching issue.
<!-- NEXT_JS_LLM_PR -->
0 commit comments