vitest-dev/vitest · error · Error
process.exit unexpectedly called with "${code}"
Error message
process.exit unexpectedly called with "${code}" What it means
Vitest deliberately overrides process.exit inside the module-runner setup (base.ts:30) so that any code under test calling process.exit throws instead of killing the whole worker process. The thrown Error carries the exit code the code tried to use. This preserves test isolation so one test cannot terminate the worker and mask other failures.
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 d568f8ce37)
Solutions
- Stub process.exit in the test (vi.stubEnv/spyOn) so it resolves instead of exiting, then assert it was called.
- Refactor the code under test to throw an error instead of calling process.exit, and assert on the thrown error.
- If the exit is in a dependency you cannot change, mock that module via vi.mock.
Example fix
// before: code under test exits
function parse(args) {
if (!args[0]) process.exit(1)
}
// after: throw instead, or spy in the test
// code
function parse(args) {
if (!args[0]) throw new Error('missing arg')
}
// test
const spy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') })
expect(() => parse([])).toThrow('missing arg') Defensive patterns
Strategy: try-catch
Try / catch
const exitSpy = vi.spyOn(process, 'exit')
.mockImplementation((code?: number) => {
throw new Error(`process.exit(${code}) called`)
})
try {
await runCodeUnderTest()
} catch (e) {
expect((e as Error).message).toMatch(/process\.exit/)
} finally {
exitSpy.mockRestore()
} Prevention
- Never call process.exit inside library code; throw a typed error instead so callers can react.
- In tests, always vi.spyOn(process,'exit') before invoking code that may exit.
- Audit dependencies for process.exit usage when adding them to a test runtime.
When it happens
Trigger: A test file or a module it imports (directly or transitively) invokes process.exit() / process.exit(code) during collection or execution under the threads/forks pool with the base environment. The override is installed in startModuleRunner and stays active while tests run.
Common situations: Testing a CLI module or library that calls process.exit on invalid input or error paths; importing a third-party dependency that hard-exits on a feature flag or missing config; code that uses process.exit as control flow.
Related errors
- process.exit unexpectedly called with "${code}"
- Isolated tasks should not share runners
- Failed to load custom "defines": ${error.message}
AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03).
Data as JSON: /data/errors/69693a6a17ae1098.json.
Report an issue: GitHub.