vitest-dev/vitest · error · SyntaxError

expect.poll() is not supported in combination with .${key}()

Error message

expect.poll() is not supported in combination with .${key}(). Use vi.waitFor() if your assertion condition is unstable.

What it means

`.poll` retries a matcher until it passes within a timeout, but several matchers are semantically incompatible with retrying: snapshot matchers would always pass once the poll callback doesn't throw, and `toThrow`-style matchers can never succeed because poll keeps invoking the callback until it stops throwing. Vitest blocks these combinations with a `SyntaxError` and points to `vi.waitFor()` for unstable non-assertion conditions.

Source

Thrown at packages/vitest/src/integrations/chai/poll.ts:82

    chai.util.flag(assertion, '_poll.interval', interval)
    const test = chai.util.flag(assertion, 'vitest-test') as Test | undefined
    if (!test) {
      throw new Error('expect.poll() must be called inside a test')
    }
    const proxy: any = new Proxy(assertion, {
      get(target, key, receiver) {
        const assertionFunction = Reflect.get(target, key, receiver)

        if (typeof assertionFunction !== 'function') {
          return assertionFunction instanceof chai.Assertion ? proxy : assertionFunction
        }

        if (key === 'assert') {
          return assertionFunction
        }

        if (typeof key === 'string' && unsupported.includes(key)) {
          throw new SyntaxError(
            `expect.poll() is not supported in combination with .${key}(). Use vi.waitFor() if your assertion condition is unstable.`,
          )
        }

        // Core poll stack-trace trick:
        //   1. capture STACK_TRACE_ERROR here before entering the async poll loop
        //   2. when the matcher eventually fails, rethrow via throwWithCause()
        //      so the final error keeps this earlier stack
        //
        // For example, when user writes:
        //    await expect.poll(...).toBeSomething()
        // STACK_TRACE_ERROR.stack would look like
        //   at ...(more internal stacks)...
        //   at __VITEST_POLL_CHAIN__ .../packages/vitest/dist/...
        //   at .../my-file.test.ts:12:3   (this points to `toBeSomething()` callsite in user test file)
        // Vitest later filters out internal stacks from `vitest/dist`, so the reported errors correctly
        // points to the user callsite for poll assertion errors.
        //

View on GitHub (pinned to d568f8ce37)

Solutions

  1. For eventual-value assertions, poll on a stable matcher (`.toBe`, `.toEqual`, etc.) and call `toMatchSnapshot` separately after the value resolves.
  2. For an unstable condition that is not an assertion, use `vi.waitFor(() => condition)` or `vi.waitUntil(() => condition)`.
  3. For eventual throw behavior, await the action inside poll and assert the resolved/thrown value with a non-throw matcher.

Example fix

// before
await expect.poll(() => readVal()).toMatchSnapshot()
// after
const val = await vi.waitFor(() => readVal())
expect(val).toMatchSnapshot()
Defensive patterns

Strategy: validation

Validate before calling

const unsupported = ['matchSnapshot','toMatchSnapshot','toMatchInlineSnapshot','toThrowErrorMatchingSnapshot','toThrowErrorMatchingInlineSnapshot','throws','Throw','throw','toThrow','toThrowError']
// review your poll chains: never append a name from `unsupported`

Prevention

When it happens

Trigger: Chaining any of: `matchSnapshot`, `toMatchSnapshot`, `toMatchInlineSnapshot`, `toThrowErrorMatchingSnapshot`, `toThrowErrorMatchingInlineSnapshot`, `throws`, `Throw`, `throw`, `toThrow`, `toThrowError` after `expect.poll(...)`, e.g. `await expect.poll(fn).toMatchSnapshot()`.

Common situations: Migrating a flaky snapshot test to poll, or trying to assert that something eventually throws by combining poll with `.toThrow()`.

Related errors


AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03). Data as JSON: /data/errors/f6faef0c0793d8dc.json. Report an issue: GitHub.