{"id":"5c15ed8c941e53e2","repo":"vitejs/vite","slug":"module-runner-hmr-client-was-closed","errorCode":null,"errorMessage":"[module runner] HMR client was closed.","messagePattern":"\\[module runner\\] HMR client was closed\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vite/src/module-runner/runner.ts","lineNumber":392,"sourceCode":"    // disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710\n    const href = posixPathToFileHref(modulePath)\n    const meta = await createImportMeta(modulePath)\n    const exports = Object.create(null)\n    Object.defineProperty(exports, Symbol.toStringTag, {\n      value: 'Module',\n      enumerable: false,\n      configurable: false,\n    })\n\n    mod.exports = exports\n\n    let hotContext: ViteHotContext | undefined\n    if (this.hmrClient) {\n      Object.defineProperty(meta, 'hot', {\n        enumerable: true,\n        get: () => {\n          if (!this.hmrClient) {\n            throw new Error(`[module runner] HMR client was closed.`)\n          }\n          this.debug?.('[module runner] creating hmr context for', mod.url)\n          hotContext ||= new HMRContext(this.hmrClient, mod.url)\n          return hotContext\n        },\n        set: (value) => {\n          hotContext = value\n        },\n      })\n    }\n\n    const context: ModuleRunnerContext = {\n      [ssrImportKey]: request,\n      [ssrDynamicImportKey]: dynamicRequest,\n      [ssrModuleExportsKey]: exports,\n      [ssrExportAllKey]: (obj: any) => exportAll(exports, obj),\n      [ssrExportNameKey]: (name, getter) =>\n        Object.defineProperty(exports, name, {","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/vitejs/vite/blob/89620f09afcfef6b35e7bb8660132ab5b4d0cd3b/packages/vite/src/module-runner/runner.ts#L374-L410","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — unguarded hot access in a callback\nsetInterval(() => {\n  import.meta.hot.dispose(() => cleanup())\n}, 1000)\n// after — guard access\nsetInterval(() => {\n  if (!runner.isClosed() && import.meta.hot) {\n    import.meta.hot.dispose(() => cleanup())\n  }\n}, 1000)","handlingStrategy":"validation","validationCode":"// Guard HMR access in deferred callbacks\nasync function safeHotAccess(runner, fn) {\n  if (runner.isClosed()) {\n    console.warn('Runner closed, skipping HMR operation')\n    return\n  }\n  return fn()\n}\n\n// usage: safeHotAccess(runner, () => import.meta.hot?.dispose(cleanup))","typeGuard":"function isHmrAvailable(runner: ModuleRunner): boolean {\n  return !runner.isClosed() && !!runner.hmrClient\n}","tryCatchPattern":"// In module code with deferred HMR access\nfunction safeHotDispose(callback) {\n  try {\n    if (import.meta.hot) {\n      import.meta.hot.dispose(callback)\n    }\n  } catch (e) {\n    if (e.message.includes('HMR client was closed')) {\n      // runner was closed; nothing to dispose\n      return\n    }\n    throw e\n  }\n}","preventionTips":["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."],"tags":["module-runner","hmr","lifecycle","close","callback"],"analyzedSha":"89620f09afcfef6b35e7bb8660132ab5b4d0cd3b","analyzedAt":"2026-08-03T19:28:02.920Z","schemaVersion":2}