vitest-dev/vitest · error · Error

process.exit unexpectedly called with

Error message

process.exit unexpectedly called with "${code}"

What it means

Identical override to error [366] but installed in the vm worker setup path (runtime/workers/vm.ts). Vitest reassigns process.exit in the vm worker so that code under test calling process.exit(code) throws instead of killing the vm worker, preserving worker/caches for subsequent test files.

Solutions

  1. Spy on process.exit in the test: vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') }).
  2. Refactor code under test to throw instead of process.exit.
  3. Inject a callback/hook the code calls instead of process.exit.
  4. Run the exiting path in a child process if real exit semantics are required.

Example fix

// before: under test
if (!cfg) process.exit(1)

// after: test spies on exit
vi.spyOn(process, 'exit').mockImplementation(c => { throw new Error('exit ' + c) })
expect(() => run()).toThrow('exit 1')
Defensive patterns

Strategy: validation

Validate before calling

import { vi } from 'vitest'
beforeEach(() => {
  vi.spyOn(process, 'exit').mockImplementation((c) => { throw new Error(`exit ${c}`) })
})

Try / catch

try {
  runUnderVm()
} catch (e) {
  if (/process\.exit unexpectedly called/.test(e.message)) {
    // assert exit code; the worker stays alive
  } else throw e
}

Prevention

When it happens

Trigger: Application code or a dependency invoked inside the vm context calls process.exit(). Because the vm worker overrides process.exit to throw, the call surfaces as 'process.exit unexpectedly called with "<code>"'.

Common situations: Same as [366]: testing a CLI/library that hard-exits on errors, a dependency exiting on missing config, or test helpers using process.exit, now exercised under the vm pool.

Related errors


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

Appendix: source

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

  // TODO: don't hardcode setImmediate in fake timers defaults
  context.setImmediate = setImmediate
  context.clearImmediate = clearImmediate

  const stubs = getDefaultRequestStubs(context)

  const externalModulesExecutor = new ExternalModulesExecutor({
    context,
    fileMap,
    codeCache,
    resolveCache,
    moduleInfoCache,
    packageCache,
    transform: rpc.transform,
    viteClientModule: stubs['/@vite/client'],
  })

  process.exit = (code = process.exitCode || 0): never => {
    throw new Error(`process.exit unexpectedly called with "${code}"`)
  }

  listenForErrors(() => state)

  const moduleRunner = startVitestModuleRunner({
    context,
    evaluatedModules: state.evaluatedModules,
    state,
    externalModulesExecutor,
    createImportMeta: createNodeImportMeta,
    traces,
  })

  emitModuleRunner(moduleRunner as any)

  Object.defineProperty(context, VITEST_VM_CONTEXT_SYMBOL, {
    value: {
      context,

View on GitHub (pinned to 1fa9837ec2)