vitest-dev/vitest · error · Error

Pool " " cannot run with "experimental.viteModuleRunner…

Error message

Pool "${context.pool}" cannot run with "experimental.viteModuleRunner: false". Please, use "threads" or "forks" instead.

What it means

setupVmWorker is the entry for the vm pool. It hard-requires experimental.viteModuleRunner to NOT be false, because the vm pool is built on Vitest's own module runner. If a config sets experimental.viteModuleRunner:false while the pool is vm/vmThreads, it throws telling the user to switch to threads or forks instead.

Solutions

  1. Remove experimental.viteModuleRunner:false (or set it true/omit it) when using pool vm/vmThreads.
  2. If you must disable the vite module runner, switch the pool to threads or forks: test.pool='threads'.
  3. Review the experimental config block and align pool choice with the runner setting.

Example fix

// before
export default defineConfig({
  test: { pool: 'vmThreads', experimental: { viteModuleRunner: false } }
})

// after
export default defineConfig({
  test: { pool: 'threads', experimental: { viteModuleRunner: false } }
})
Defensive patterns

Strategy: validation

Validate before calling

function assertPoolRunnerCompatible(pool, experimental) {
  if (pool.startsWith('vm') && experimental?.viteModuleRunner === false) {
    throw new Error('vm pool requires viteModuleRunner; use threads/forks')
  }
}

Type guard

function configSupportsVmPool(cfg) {
  return cfg.experimental?.viteModuleRunner !== false
}

Prevention

When it happens

Trigger: Configuration combines test.pool='vm' (or 'vmThreads') with test.experimental.viteModuleRunner=false. The vm pool cannot operate without the Vitest module runner, so this combination is rejected at worker setup.

Common situations: Disabling the vite module runner to fall back to Vite's native loading while forgetting that the vm pool depends on it; copy-pasting an experimental config block from a threads-based project into a vm-pool project.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/8cd0d6efe46db585. Report an issue: GitHub.

Appendix: source

Thrown at packages/vitest/src/runtime/workers/vm.ts:193

  }
  finally {
    await traces.$(
      'vitest.runtime.environment.teardown',
      () => vm.teardown?.(),
    )
    // unregisters the runner from Vite's `Error.prepareStackTrace` interceptor:
    // its module-level cache holds `evaluatedModules` of every runner it has
    // seen, which would otherwise keep each test file's entire module graph
    // (and with it the vm context) alive for the lifetime of the worker
    await moduleRunner.close()
    stripDisposedContext(context, initialContextKeys)
    setActiveVmExecutor(undefined)
  }
}

export function setupVmWorker(context: WorkerSetupContext): void {
  if (context.config.experimental.viteModuleRunner === false) {
    throw new Error(`Pool "${context.pool}" cannot run with "experimental.viteModuleRunner: false". Please, use "threads" or "forks" instead.`)
  }
  // V8's isolate-level compilation cache keeps evaluated `vm.SourceTextModule`s
  // (and everything their module state references) alive until a
  // memory-pressure GC clears the cache, which in practice means every test
  // file's world accumulates until the worker hits `vmMemoryLimit`. The
  // compiled-code caching the flag disables is already covered by the
  // worker's own script and code caches.
  v8.setFlagsFromString('--no-compilation-cache')
}

View on GitHub (pinned to 1fa9837ec2)