vitest-dev/vitest · error · TypeError
is not a `vi.when` instance
Error message
${utils.inspect(chain)} is not a `vi.when` instance What it means
Thrown by the `toHaveBeenExhausted` Chai assertion when the asserted value is not a chain returned by `vi.when(...)`. The assertion inspects the object via `isWhenChain` (which checks for the internal `$$vitest:when` symbol); if the symbol is absent it raises a TypeError. It exists to stop developers from calling the assertion on a plain mock, spy, or unrelated value, since exhaustion tracking is only meaningful for `vi.when` behaviors.
Solutions
- Capture the return of `vi.when(spy)` and assert against that: `const w = vi.when(spy); ...; expect(w).toHaveBeenExhausted()`.
- If you intended to check call counts instead, use `expect(spy).toHaveBeenCalledTimes(n)`.
- Remove the `toHaveBeenExhausted` assertion if no `vi.when` behavior was set up.
Example fix
// before
const spy = vi.fn()
vi.when(spy).calledWith(1).thenReturn('a')
expect(spy).toHaveBeenExhausted()
// after
const spy = vi.fn()
const w = vi.when(spy).calledWith(1).thenReturn('a')
expect(w).toHaveBeenExhausted() Defensive patterns
Strategy: type-guard
Validate before calling
import { isWhenChain } from 'vitest/vi' // or from the mock internals
if (!isWhenChain(target)) {
throw new Error('Pass the vi.when(...) return value, not the mock itself')
}
expect(target).toHaveBeenExhausted() Type guard
import { isWhenChain } from '@vitest/spy'
function assertWhenChain(v: unknown) {
if (!isWhenChain(v as object)) throw new TypeError('not a vi.when instance')
return v
} Try / catch
try {
expect(target).toHaveBeenExhausted()
} catch (e) {
if (e instanceof TypeError && /vi\.when/.test(e.message)) {
// re-route: capture the vi.when return value before asserting
}
throw e
} Prevention
- Always assign the return of vi.when(spy) to a variable and assert against that variable.
- Add a lint rule or code-review check that toHaveBeenExhausted is only called on a vi.when result.
- Write a small wrapper that accepts the when-chain and throws a clearer error if a non-chain is passed.
When it happens
Trigger: Calling `expect(spy).toHaveBeenExhausted()` where `spy` is a `vi.fn()`/`vi.spyOn()` result instead of the object returned by `vi.when(spy)`; calling it on a mock that was never passed through `vi.when`; passing a non-mock value like a plain object or number.
Common situations: Confusing the mock function with the `vi.when` return value; refactoring code so the `when` result is no longer in scope; copy-pasting an assertion without setting up `vi.when`.
Related errors
- vi.when: the argument must be a mock function created with…
- vi.when: no behavior defined when called with
- aria adapter expects an Element
- Cannot spy on export
- expected must be a function, received
AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11).
Data as JSON: /api/errors/513e08af97aed934.
Report an issue: GitHub.
Appendix: source
Thrown at packages/vitest/src/integrations/mock/chai.ts:13
import type { ChaiPlugin } from '@vitest/expect'
import { wrapAssertion } from '@vitest/expect'
import { isWhenChain } from './when'
export const MockPlugin: ChaiPlugin = (chai, utils) => {
utils.addMethod(
chai.Assertion.prototype,
'toHaveBeenExhausted',
wrapAssertion(utils, 'toHaveBeenExhausted', function (this) {
const chain = utils.flag(this, 'object')
if (!isWhenChain(chain)) {
throw new TypeError(
`${utils.inspect(chain)} is not a \`vi.when\` instance`,
)
}
const diagnostics = chain._getDiagnostics()
this.assert(
diagnostics.isExhausted,
`expected all behaviors to have been exhausted, but some remain:\n\n ${diagnostics.pendingBehaviors.replaceAll(/\n(?!\n)/g, '\n ')}`,
'expected at least one behavior to remain un-exhausted, but all were',
)
}),
)
}
View on GitHub (pinned to 1fa9837ec2)