vitejs/vite · error · Error
Vite module runner has been closed.
Error message
Vite module runner has been closed.
What it means
Thrown in getModuleInformation when attempting to fetch a module after runner.close() has been called. The close() method sets this.closed = true, clears caches, removes HMR listeners, and disconnects the transport. Any subsequent import() call that triggers a module fetch hits this guard because the runner is no longer operational.
Source
Thrown at packages/vite/src/module-runner/runner.ts:273
builtin,
)
this.isBuiltin = createIsBuiltin(builtins)
this.debug?.('[module runner] builtins loaded:', builtins)
} finally {
this.builtinsPromise = undefined
}
})()
return this.builtinsPromise
}
private async getModuleInformation(
url: string,
importer: string | undefined,
cachedModule: EvaluatedModuleNode | undefined,
): Promise<EvaluatedModuleNode> {
if (this.closed) {
throw new Error(`Vite module runner has been closed.`)
}
await this.ensureBuiltins()
this.debug?.('[module runner] fetching', url)
const isCached = !!(typeof cachedModule === 'object' && cachedModule.meta)
const fetchedModule = // fast return for established externalized pattern
(
url.startsWith('data:') || this.isBuiltin?.(url)
? { externalize: url, type: 'builtin' }
: await this.transport.invoke('fetchModule', [
url,
importer,
{
cached: isCached,
startOffset: this.evaluator.startOffset,View on GitHub (pinned to 89620f09af)
Solutions
- Check runner.isClosed() before attempting to import: if (!runner.isClosed()) { await runner.import(url) }.
- Ensure close() is only called after all pending imports have resolved — track in-flight operations.
- If using the runner for SSR, create a new runner instance for each request lifecycle rather than reusing a closed one.
- Restructure shutdown logic so close() is the last operation, called after all request handling is complete.
Example fix
// before — import after close throws
await runner.close()
await runner.import('/src/app.ts') // throws
// after — guard against closed state
if (!runner.isClosed()) {
await runner.import('/src/app.ts')
} else {
// recreate runner or handle gracefully
} Defensive patterns
Strategy: validation
Validate before calling
// Guard imports against closed runner state
async function safeImport(runner, url) {
if (runner.isClosed()) {
throw new Error(`Cannot import ${url}: runner is closed. Create a new instance.`)
}
return runner.import(url)
} Type guard
function isRunnerActive(runner: ModuleRunner): boolean {
return !runner.isClosed()
} Try / catch
try {
await runner.import(url)
} catch (e) {
if (e.message.includes('has been closed')) {
// recreate runner or skip
console.warn('Runner closed, skipping import')
return null
}
throw e
} Prevention
- Always call runner.isClosed() before import() in long-lived async code.
- Track in-flight import promises and await them all before calling close().
- Use a wrapper that queues or rejects imports after close rather than letting them throw internally.
- Structure SSR code so the runner lifecycle matches request lifecycle.
When it happens
Trigger: Calling runner.import() or runner.import(url) after runner.close() has been awaited. Common in test teardown, SSR request handlers that close the runner prematurely, or when a long-running promise tries to import a module after the runner lifecycle has ended.
Common situations: In SSR or test environments, the runner is closed during shutdown but an in-flight async operation (e.g., a pending import, a lazy-loaded route handler) attempts to load a module after close. Also happens with incorrect lifecycle management where close() is called too early.
Related errors
- [module runner] HMR client was closed.
- [module runner] Dynamic access of "import.meta.env" is not s
- [module runner] "import.meta.resolve" is not supported.
- [module runner] "import.meta.glob" is statically replaced du
- Module "${url}" was mistakenly invalidated during fetch phas
AI-assisted analysis of vitejs/vite@89620f09af (2026-08-03).
Data as JSON: /data/errors/f15a9abd2cfe6424.json.
Report an issue: GitHub.