Turbopack: don't crash the dev server when node_modules/next is briefly unresolvable#93877
Turbopack: don't crash the dev server when node_modules/next is briefly unresolvable#93877lukesandberg wants to merge 3 commits into
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
d067f28 to
e2a194f
Compare
Stats from current PR✅ No significant changes detected📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📎 Tarball URLCommit: e2a194f |
Failing test suitesCommit: baf65c3 | About building and testing Next.js
Expand output● _app/_document removal HMR › should HMR when _app is removed
Expand output● ReactRefreshLogBox app › server component can recover from error thrown in the module
Expand output● tsconfig-path-reloading › tsconfig added after starting dev › should recover from module not found when paths is updated
Expand output● ReactRefreshRequire › propagates a module that stops accepting in next version
Expand output● _app/_document removal HMR › should HMR when _app is removed
Expand output● jsconfig-path-reloading › jsconfig › should recover from module not found when paths is updated ● jsconfig-path-reloading › jsconfig added after starting dev › should recover from module not found when paths is updated
Expand output● ReactRefreshRegression › can fast refresh a page with getStaticProps ● ReactRefreshRegression › can fast refresh a page with getServerSideProps ● ReactRefreshRegression › can fast refresh a page with config
Expand output● Error recovery app › can recover from a syntax error without losing state ● Error recovery app › server component can recover from a component error ● Error recovery app › render error not shown right after syntax error
Expand output● pages/ error recovery › logbox: can recover from a syntax error without losing state ● pages/ error recovery › render error not shown right after syntax error
Expand output● typescript-auto-install › should detect TypeScript being added and auto setup
Expand output● Instrumentation Client Hook › HMR in development mode › should reload instrumentation-client when modified Other failing CI jobs |
add a missing root annotation exempt webpack from the concurrent install test
e2a194f to
baf65c3
Compare

What?
When
node_modules/nextis briefly unresolvable mid-session (for example because pnpm is mid-install of a package with anextpeer dependency, reshuffling its symlinks) Turbopack would:MissingNextFolderIssuewith severityFatalanyhow!("Next.js package not found")through ~10 levels of the build graphTurbopackInternalError, which the Next.js JS-side treats as fatal and shuts the dev server downThe 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 devgetting stuck in a bad state. Still this PR will improve a number of apis and error messagesSee 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,
nextis resolvable again. There is no reason this should bring down the dev server or poison the persistent cache.The bug had two contributing causes:
The error message was poor. It assumed the only cause was a mis-set
turbopack.root, with the entire message intitle(). It didn't acknowledge the install-race case or any of the other realistic causes (broken symlink, monorepo hoisting, removednode_modules/next, global install). Users had no way to know that the right response was to wait or refresh.The Rust→napi boundary did not treat this as a recoverable error. Two specific code paths propagated
Errinstead of catching it:issue_filter_from_endpointinendpoint.rscallsendpoint_op.connect().await?— if the upstream endpoint resolution errors (as it does whendirectory_tree_to_loader_treefails), this?propagates before the surroundingstrongly_consistent_catch_collectablesgets a chance to convert the failure into Issues.Project::hmr_version_stateis called bare fromproject_hmr_eventsat the napi boundary. There is no_with_issues_operationwrapper, so any error from the underlyinghmr_content/versioned_content_map.compute_entrychain propagates straight to JS.How?
Three changes:
1. Rewrite
MissingNextFolderIssue(next_import_map.rs)FataltoError.Fataltriggers an explicit JS-side server shutdown inprint-build-errors.ts;Errorlets the dev server recover."Could not find the Next.js package (next/package.json)".documentation_link()so it doesn't pollute the inline message.2. Make
issue_filter_from_endpointErr-tolerant (endpoint.rs)endpoint_op.connect().awaitin amatchand fall back toIssueFilter::warnings_and_foreign_errors()onErr.strongly_consistent_catch_collectablesrecovery 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)hmr_update_with_issues_operationpattern.hmr_version_operationas anOperationVc, catches anyErrand falls back toNotFoundVersion, and collects issues from the operation viapeek_issues.project_hmr_eventshandler now uses this wrapper instead of callingProject::hmr_version_statebare. Issues from bothhmr_version_stateandhmr_updateare merged before being sent to JS.hmr_version_operationout ofProject::hmr_version_state's closure to make it callable from the napi layer.A new regression test at test/development/app-dir/concurrent-install/ reproduces the failure by moving
node_modules/nextaside mid-HMR-session and asserts that the dev server does not emitFATAL: An unexpected Turbopack error occurredorTurbopackInternalError, and that the friendly Issue text surfaces. The test is skipped underNEXT_SKIP_ISOLATE=1since that mode doesn't produce a manipulablenode_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.