vitejs/vite · error · Error
[module runner] HMR client was closed.
Error message
[module runner] HMR client was closed.
What it means
Thrown in the import.meta.hot property getter (inside directRequest) when module code accesses import.meta.hot after the runner has been closed. The close() method sets this.hmrClient = undefined, so any subsequent access to the hot context throws. Module code that lazily accesses import.meta.hot (e.g., in a callback or conditional) can hit this if the runner was closed between module evaluation and the hot access.
Source
Thrown at packages/vite/src/module-runner/runner.ts:392
// disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710
const href = posixPathToFileHref(modulePath)
const meta = await createImportMeta(modulePath)
const exports = Object.create(null)
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module',
enumerable: false,
configurable: false,
})
mod.exports = exports
let hotContext: ViteHotContext | undefined
if (this.hmrClient) {
Object.defineProperty(meta, 'hot', {
enumerable: true,
get: () => {
if (!this.hmrClient) {
throw new Error(`[module runner] HMR client was closed.`)
}
this.debug?.('[module runner] creating hmr context for', mod.url)
hotContext ||= new HMRContext(this.hmrClient, mod.url)
return hotContext
},
set: (value) => {
hotContext = value
},
})
}
const context: ModuleRunnerContext = {
[ssrImportKey]: request,
[ssrDynamicImportKey]: dynamicRequest,
[ssrModuleExportsKey]: exports,
[ssrExportAllKey]: (obj: any) => exportAll(exports, obj),
[ssrExportNameKey]: (name, getter) =>
Object.defineProperty(exports, name, {View on GitHub (pinned to 89620f09af)
Solutions
- Ensure all timers, intervals, and event listeners that might access import.meta.hot are cleaned up before calling runner.close().
- Guard import.meta.hot access in module code: if (import.meta.hot) { ... } — though note this checks existence, not closure.
- Use runner.isClosed() checks in long-lived callbacks before performing HMR operations.
- Set hmr: false on the runner if you don't need HMR, which prevents the hot property from being defined at all.
Example fix
// before — unguarded hot access in a callback
setInterval(() => {
import.meta.hot.dispose(() => cleanup())
}, 1000)
// after — guard access
setInterval(() => {
if (!runner.isClosed() && import.meta.hot) {
import.meta.hot.dispose(() => cleanup())
}
}, 1000) Defensive patterns
Strategy: validation
Validate before calling
// Guard HMR access in deferred callbacks
async function safeHotAccess(runner, fn) {
if (runner.isClosed()) {
console.warn('Runner closed, skipping HMR operation')
return
}
return fn()
}
// usage: safeHotAccess(runner, () => import.meta.hot?.dispose(cleanup)) Type guard
function isHmrAvailable(runner: ModuleRunner): boolean {
return !runner.isClosed() && !!runner.hmrClient
} Try / catch
// In module code with deferred HMR access
function safeHotDispose(callback) {
try {
if (import.meta.hot) {
import.meta.hot.dispose(callback)
}
} catch (e) {
if (e.message.includes('HMR client was closed')) {
// runner was closed; nothing to dispose
return
}
throw e
}
} Prevention
- Always clean up timers, intervals, and listeners before calling runner.close().
- Guard import.meta.hot access in deferred callbacks with runner state checks.
- Set hmr: false if HMR isn't needed, which prevents the hot getter from being defined.
- Use a disposal pattern that unregisters all HMR callbacks on shutdown.
When it happens
Trigger: Module code accesses import.meta.hot inside a deferred callback (event handler, setTimeout, etc.) that fires after runner.close(). The module was loaded successfully, HMR context was available during load, but by the time the callback runs, the runner is closed and hmrClient is undefined.
Common situations: SSR applications with HMR enabled where the server shuts down (closing the runner) but lingering timers, intervals, or event listeners in loaded modules still reference import.meta.hot. Vitest test environments where the runner is torn down but async callbacks in test modules haven't been cleaned up.
Related errors
- Vite module runner has been closed.
- HMR is not supported by this runner transport, but `hmr` opt
- Module "${url}" was mistakenly invalidated during fetch phas
- [module runner] Dynamic access of "import.meta.env" is not s
- [module runner] "import.meta.resolve" is not supported.
AI-assisted analysis of vitejs/vite@89620f09af (2026-08-03).
Data as JSON: /data/errors/5c15ed8c941e53e2.json.
Report an issue: GitHub.