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

  1. Await all in-flight imports before calling `runner.close()` or `runner.clearCache()`.
  2. Guard HMR usage by checking `runner.isClosed()` from outside the module, or wrap `import.meta.hot` access in try/catch.
  3. 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

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


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)