vitest-dev/vitest · error · Error
[vitest-pool-runner]: Cannot start a stopped runner
Error message
[vitest-pool-runner]: Cannot start a stopped runner
What it means
Thrown by PoolRunner.start when the runner's state is already STOPPED. A runner that has been stopped has torn down its worker process/thread and closed its RPC channel, so it cannot be restarted; Vitest always creates a new PoolRunner for a fresh task rather than reusing a stopped one.
Source
Thrown at packages/vitest/src/node/pools/poolRunner.ts:180
private getOTELCarrier() {
const activeContext = this._otel?.currentContext || this._otel?.workerContext
return activeContext
? this._traces.getContextCarrier(activeContext)
: undefined
}
async start(options: { workerId: number }): Promise<void> {
// Wait for any ongoing operation to complete
if (this._operationLock) {
await this._operationLock
}
if (this._state === RunnerState.STARTED || this._state === RunnerState.STARTING) {
return
}
if (this._state === RunnerState.STOPPED) {
throw new Error('[vitest-pool-runner]: Cannot start a stopped runner')
}
// Create operation lock to prevent concurrent start/stop
this._operationLock = createDefer()
let startSpan: Span | undefined
try {
this._state = RunnerState.STARTING
await this._traces.$(
`vitest.${this.worker.name}.start`,
{ context: this._otel?.workerContext },
() => this.worker.start(),
)
// Attach event listeners AFTER starting worker to avoid issues
// if worker.start() fails
this.worker.on('error', this.emitWorkerError)View on GitHub (pinned to d568f8ce37)
Solutions
- Report as a Vitest bug if seen during normal CLI usage (this is an internal invariant).
- If using PoolRunner directly (experimental API), never call start() after stop(); construct a new PoolRunner instead.
- Retry — transient teardown races during cancellation can trigger this once.
Example fix
// before (experimental API misuse): reusing a stopped runner
await runner.stop()
await runner.start({ workerId: 1 }) // throws
// after: construct a new runner
await runner.stop()
const fresh = new PoolRunner(options, new ForksPoolWorker(options))
await fresh.start({ workerId: 1 }) Defensive patterns
Strategy: validation
Validate before calling
// Experimental PoolRunner API: never start a stopped runner
function assertCanStart(runner) {
if (runner.isTerminated) {
throw new Error('Runner is stopped; create a new PoolRunner instead of restarting.')
}
} Type guard
function isReusableRunner(runner) {
return !runner.isTerminated
} Try / catch
try {
await runner.start({ workerId: 1 })
} catch (e) {
if (e.message === '[vitest-pool-runner]: Cannot start a stopped runner') {
// construct a fresh runner instead of retrying
throw new Error('Runner was stopped; recreate it before starting')
}
throw e
} Prevention
- Never call start() after stop() on a PoolRunner; always construct a new one.
- Treat PoolRunner as @experimental and avoid direct lifecycle manipulation.
- Report internal cases seen during normal CLI usage as bugs.
When it happens
Trigger: runner.start() is called after runner.stop() completed (state === STOPPED), at packages/vitest/src/node/pools/poolRunner.ts:179-180. Normally Pool.getPoolRunner constructs a new runner for stopped workers, so this implies reuse of a stopped instance.
Common situations: An internal bug where a stopped runner is retained in sharedRunners and picked up again; programmatic misuse of the @experimental PoolRunner API calling start() after stop(); a cancellation/teardown race that re-schedules onto a stopped runner.
Related errors
- The child process was torn down or never initialized. This i
- The worker thread was torn down or never initialized. This i
- Cannot find the environment. This is a bug in Vitest.
- The browser server was not initialized${project.name ? ` for
- Orchestrator not found for session ${sessionId}. This is a b
AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03).
Data as JSON: /data/errors/1ec775041a810130.json.
Report an issue: GitHub.