vitest-dev/vitest · error · Error

Cannot import "${specifier}": the test context was torn down

Error message

Cannot import "${specifier}": the test context was torn down.

What it means

Vitest routes vm-pool dynamic import() through a module-level activeVmExecutor set by setActiveVmExecutor(). When a dynamic import fires after the executor has been cleared (set to undefined during context teardown), activeImportModuleDynamically() has no executor to delegate to and throws this error for the given specifier.

Source

Thrown at packages/vitest/src/runtime/vm/utils.ts:128

// The active executor of this worker: vm pools run one test file (and so
// one executor) at a time, which lets script-level dynamic import callbacks
// be static functions instead of per-executor closures. Node registers the
// callback for the lifetime of the compiled script, so a closure would both
// pin the executor's test file world and make compiled scripts unshareable
// between contexts.
interface ActiveVmExecutor {
  importModuleDynamically: (specifier: string, referencer: VMModule) => Promise<VMModule>
}

let activeVmExecutor: ActiveVmExecutor | undefined

export function setActiveVmExecutor(executor: ActiveVmExecutor | undefined): void {
  activeVmExecutor = executor
}

export async function activeImportModuleDynamically(specifier: string, referencer: VMModule): Promise<VMModule> {
  if (!activeVmExecutor) {
    throw new Error(`Cannot import "${specifier}": the test context was torn down.`)
  }
  return activeVmExecutor.importModuleDynamically(specifier, referencer)
}

// Node never collects a vm context in which multiple scripts installed
// closures, and `vm.SourceTextModule`s are pinned by the realm's base object
// list: the ContextifyContext/ModuleWrap wrappers keep the whole context
// reachable even through forced full GCs, so a long-lived vm worker
// accumulates every test file's world until it hits `vmMemoryLimit` and gets
// recycled, destroying the worker's compile caches with it. Clearing what the
// test file added to the global object (and the DOM) caps what a pinned
// context retains. Pristine globals are kept so that work queued before the
// teardown (jsdom events, worker-scoped fixture cleanups) can still run.
const captureKeysScript = new vm.Script(
  `Object.getOwnPropertyNames(globalThis).concat(Object.getOwnPropertySymbols(globalThis))`,
  { filename: 'virtual:vitest-capture-context-keys.js' },
)

View on GitHub (pinned to 1fa9837ec2)

Solutions

  1. Ensure all async work in the test is awaited before the test completes so no import() runs post-teardown.
  2. Disable worker reuse for isolation: test.isolate=true (default) or test.maxWorkers low enough to recycle workers per file.
  3. Use vi.useFakeTimers or properly clear timers/listeners in afterEach to stop deferred callbacks from firing later.
  4. Switch to a non-vm pool (threads/forks) if the late import is unavoidable and the test does not depend on vm semantics.

Example fix

// before: import fires after teardown
it('lazy', () => {
  setTimeout(() => import('./late'), 1000)
})

// after: await the import inside the test
it('lazy', async () => {
  await import('./late')
})
Defensive patterns

Strategy: validation

Validate before calling

import { setActiveVmExecutor } from './utils'
// before a deferred import, confirm the executor is still active
if (!getActiveVmExecutor()) {
  // skip or queue the import; context is gone
  return
}

Type guard

function canImportDynamically() {
  return typeof activeVmExecutor?.importModuleDynamically === 'function'
}

Try / catch

try {
  await import(specifier)
} catch (e) {
  if (/the test context was torn down/.test(e.message)) {
    // swallow or reschedule; do not retry blindly
    return null
  }
  throw e
}

Prevention

When it happens

Trigger: A pending microtask, setTimeout callback, or unresolved promise inside a test triggers `await import('...')` after the current test file's vm context has already been disposed. The import is routed through activeImportModuleDynamically, which sees activeVmExecutor === undefined.

Common situations: Async cleanup that outlives the test (a setInterval, an unawaited promise, an event listener), or code under test that lazily imports modules after the file finishes. Also seen when a worker is reused and the previous file's lingering callbacks fire.

Related errors


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