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

  1. Check runner.isClosed() before attempting to import: if (!runner.isClosed()) { await runner.import(url) }.
  2. Ensure close() is only called after all pending imports have resolved — track in-flight operations.
  3. If using the runner for SSR, create a new runner instance for each request lifecycle rather than reusing a closed one.
  4. 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

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


AI-assisted analysis of vitejs/vite@89620f09af (2026-08-03). Data as JSON: /data/errors/f15a9abd2cfe6424.json. Report an issue: GitHub.