vitejs/vite · error · Error
[module runner] HMR client was closed.
Error message
[module runner] HMR client was closed.
What it means
The runner installs a getter on `import.meta.hot` that throws if `this.hmrClient` is undefined. `hmrClient` is set to undefined by `clearCache()` and by `close()`, so any access to `import.meta.hot` after the HMR client is torn down triggers this error.
Solutions
- Await all in-flight imports before calling `runner.close()` or `runner.clearCache()`.
- Guard HMR usage by checking `runner.isClosed()` from outside the module, or wrap `import.meta.hot` access in try/catch.
- Run HMR-dependent modules only while the HMR client is known to be alive.
Example fix
// before - inside a module
if (import.meta.hot) {
import.meta.hot.accept('/src/dep', () => reload())
}
// after - guard against closed runner via try/catch
try {
import.meta.hot?.accept('/src/dep', () => reload())
} catch { /* runner closing/closed */ } Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure no imports are in flight when tearing down
async function safeClose(runner: { import: (u: string)=>Promise<unknown>; close: ()=>Promise<void> }, inflight: Promise<unknown>[]) {
await Promise.allSettled(inflight)
await runner.close()
} Type guard
function hmrClientAlive(runner: any): boolean {
// hmrClient is private; infer via option state tracked externally
return !runner.isClosed()
} Try / catch
try {
if (import.meta.hot) import.meta.hot.accept('/dep', cb)
} catch (e) {
if (e instanceof Error && /HMR client was closed/.test(e.message)) {
// runner is shutting down - skip HMR registration
} else throw e
} Prevention
- Close the runner only after all imports have settled.
- Don't rely solely on `if (import.meta.hot)` - the getter throws once closed; wrap in try/catch.
When it happens
Trigger: User module code reading `import.meta.hot` (e.g. `if (import.meta.hot) { import.meta.hot.accept(...) }`) executed after `runner.close()` or `runner.clearCache()` while the HMR client is gone. Note: because the getter always exists on the meta object, `if (import.meta.hot)` truthiness checks alone do not protect against it.
Common situations: Teardown ordering: closing the runner while a module that registers HMR callbacks is still being evaluated/imported. Tests that clear the cache mid-import. SSR code that conditionally uses `import.meta.hot`.
Related errors
- Cannot send non-custom events from the client to the server.
- Cannot send non-custom events from the server to the client.
- HMR is not supported by this runner transport, but `hmr`…
- invoke was called before connect
- send was called before connect
AI-assisted analysis of vitejs/vite@b4d66fee14 (2026-08-11).
Data as JSON: /api/errors/5c15ed8c941e53e2.
Report an issue: GitHub.
Appendix: 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 b4d66fee14)