fix(plugin-container): add defensive check for undefined transform handler
Description
This PR fixes an intermittent crash that causes the error Cannot read properties of undefined (reading 'call') during HMR and server restarts.
Fixes #21162
Problem
This error occurs intermittently and repeatedly, primarily when:
- Making significant code changes (mass formatting or refactoring)
- Switching branches
- Modifying
package.json
The error appears in both the browser and terminal, effectively stopping the HMR process and requiring a manual server restart. This is particularly common with Astro + Tailwind + React setups.
Root Cause
-
vite-plugin-reactintentionally removes itstransformhook during theconfigResolvedphase for performance optimization (source) - During server restarts (especially triggered by
package.jsonchanges), thepluginContainermay iterate over cached plugin references where thetransformhook has been dynamically deleted -
getHookHandler(plugin.transform)returnsundefined, and attempting to call it throws the error
Credit to @sapphi-red for identifying the root cause.
Solution
Added a defensive check in pluginContainer.ts before calling the transform handler:
const handler = getHookHandler(plugin.transform)
if (!handler) {
continue
}
Thanks for the question, happy to clarify.
What this sentence is trying to describe is a race between the dev server restart and in-flight transform calls. When package.json changes, Vite fully recreates the dev server: a new server instance (with fresh plugin instances) is created, the old server is closed, and properties from the new server are assigned back onto the existing server object. During this transition:
- The old
pluginContainercan still be running pendingtransformcalls that started before the restart. These calls hold references to the old plugin objects. - Some plugins (like
vite-plugin-react) intentionally delete or replace theirtransformhook duringconfigResolvedfor performance reasons, so on those old plugin objectsplugin.transformmay already have been removed.
If pluginContainer iterates over its internal plugin list while one of those stale plugin objects no longer has a valid transform hook, getHookHandler(plugin.transform) returns undefined, and the subsequent handler.call(...) throws Cannot read properties of undefined (reading 'call'). The defensive check simply turns this into a no-op for that plugin in this edge case, letting the rest of the pipeline continue without crashing.
Each pluginContainer has a separate cache. So I don't think your explanation makes sense. https://github.com/vitejs/vite/blob/3f344b4f13f867fa646e0562f62c7bed06549cb0/packages/vite/src/node/server/pluginContainer.ts#L212