vitest-dev/vitest · error · Error

process.exit unexpectedly called with

Error message

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

What it means

To keep workers alive across test files, Vitest reassigns process.exit in startModuleRunner to a function that throws instead of terminating the process. Any call to process.exit(code) inside test code or an imported module therefore raises this error with the requested code. The throw is caught by Vitest's worker error handling and reported as a test failure rather than killing the worker.

Solutions

  1. Spy on process.exit in the test and override it to throw a recognizable error: vi.spyOn(process, 'exit').mockImplementation(code => { throw new Error('exit ' + code) }).
  2. Refactor the code under test to throw an Error instead of calling process.exit so it is testable.
  3. Inject an exit hook/callback the code calls instead of process.exit directly (dependency injection).
  4. Run the exiting code in a subprocess when you genuinely need the real exit behavior.

Example fix

// before: code under test
deleteDb() ; process.exit(1)

// after: throw, or spy in the test
// test:
vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') })
expect(() => deleteDb()).toThrow()
Defensive patterns

Strategy: validation

Validate before calling

// In tests, replace process.exit before exercising the code
import { vi } from 'vitest'
beforeEach(() => {
  vi.spyOn(process, 'exit').mockImplementation((code) => {
    throw new Error(`process.exit(${code})`)
  })
})

Try / catch

try {
  runCodeUnderTest()
} catch (e) {
  if (/process\.exit unexpectedly called/.test(e.message)) {
    // assert the intended exit code instead of crashing
  } else throw e
}

Prevention

When it happens

Trigger: Application code or a dependency under test invokes process.exit() (commonly process.exit(1) on a fatal error). Because the override throws synchronously, the call site sees an exception with message 'process.exit unexpectedly called with "<code>"'.

Common situations: Testing a CLI or library that calls process.exit on invalid input or fatal errors; a dependency that hard-exits on missing config; or a test helper that uses process.exit. The override is intentional so workers are recycled rather than dying.

Related errors


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

Appendix: source

Thrown at packages/vitest/src/runtime/workers/base.ts:31

import { VitestEvaluatedModules } from '../moduleRunner/evaluatedModules'
import { createNodeImportMeta } from '../moduleRunner/moduleRunner'
import { startVitestModuleRunner } from '../moduleRunner/startVitestModuleRunner'
import { run } from '../runBaseTests'
import { setupEnv } from '../setup-common'
import { getSafeWorkerState, provideWorkerState } from '../utils'

let _moduleRunner: TestModuleRunner

const evaluatedModules = new VitestEvaluatedModules()
const moduleExecutionInfo = new Map()

async function startModuleRunner(options: ContextModuleRunnerOptions): Promise<TestModuleRunner> {
  if (_moduleRunner) {
    return _moduleRunner
  }

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

  listenForErrors(state)

  if (options.state.config.experimental.viteModuleRunner === false) {
    const root = options.state.config.root
    let mocker: TestModuleMocker | undefined
    if (options.state.config.experimental.nodeLoader !== false) {
      // this additionally imports acorn/magic-string
      const { NativeModuleMocker } = await import('../moduleRunner/nativeModuleMocker')
      mocker = new NativeModuleMocker({
        async resolveId(id, importer) {
          // TODO: use import.meta.resolve instead
          return state().rpc.resolve(id, importer, '__vitest__')
        },
        root,
        moduleDirectories: state().config.deps.moduleDirectories || ['/node_modules/'],

View on GitHub (pinned to 1fa9837ec2)