vitest-dev/vitest · error · RangeError

vi.when: `times` option must be greater than 0

Error message

vi.when: `times` option must be greater than 0

What it means

Thrown by `validateOptions` (called by each `then*`/`then*Once` builder) when the `times` option is a number less than or equal to zero. `times` controls how many calls a behavior applies to before being exhausted; zero or negative counts are nonsensical, so a RangeError is raised immediately at setup time rather than silently producing an always-exhausted behavior.

Solutions

  1. Use a positive integer: `times: 1` (or prefer `thenReturnOnce` for the single-call case).
  2. Guard dynamic counts: `times: Math.max(1, n)` or skip setting up the behavior when `n === 0`.
  3. Use `thenReturnOnce` instead of `times: 1` for clarity when you want exactly one application.

Example fix

// before
vi.when(spy).calledWith(1).thenReturn('a', { times: 0 })

// after
vi.when(spy).calledWith(1).thenReturnOnce('a')
Defensive patterns

Strategy: validation

Validate before calling

function validTimes(n) {
  if (typeof n === 'number' && n <= 0) throw new RangeError('times must be > 0')
  return n
}
vi.when(spy).calledWith(1).thenReturn('a', { times: validTimes(n) })

Type guard

function isPositiveTimes(n): n is number {
  return typeof n === 'number' && n > 0
}

Prevention

When it happens

Trigger: Calling `.thenReturn(value, { times: 0 })`, `.thenReturn(value, { times: -1 })`, or passing a computed `times` that underflows to zero/negative (e.g. `times: arr.length` on an empty array).

Common situations: Deriving `times` from a dynamic count (array length, counter) that can be zero; copy-paste of `times: 0` intending 'once' (should be `1` or `thenReturnOnce`); off-by-one in loop-driven stubbing.

Related errors


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

Appendix: source

Thrown at packages/vitest/src/integrations/mock/when.ts:547

      : `exhausted (${action.times} of ${action.times})`
  }

  return action.remaining === Number.POSITIVE_INFINITY
    ? 'never called'
    : `${action.remaining} remaining (out of ${action.times})`
}

function getSymbol(action: BehaviorAction<unknown>): string {
  if (hasBeenConsumed(action)) {
    return '✓'
  }

  return '✗'
}

function validateOptions(options: BehaviorOptions | undefined) {
  if (typeof options?.times === 'number' && options.times <= 0) {
    throw new RangeError('vi.when: `times` option must be greater than 0')
  }
}

View on GitHub (pinned to 1fa9837ec2)