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
- 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) }).
- Refactor the code under test to throw an Error instead of calling process.exit so it is testable.
- Inject an exit hook/callback the code calls instead of process.exit directly (dependency injection).
- 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
- Never call process.exit in test helpers; throw instead.
- Spy on process.exit in tests that exercise error paths.
- Inject an exit hook into code under test for testability.
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
- process.exit unexpectedly called with
- Browser is not initialized
- Browser provider is not defined for the project
- Calling the suite function inside test function is not…
- Calling the test function inside another test function is…
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)