vitest-dev/vitest · error · Error

Circular fixture dependency detected

Error message

Circular fixture dependency detected: ${fixture.name} <- ${[...depSet].reverse().map(d => d.name).join(' <- ')}

What it means

Thrown by resolveDeps when following a fixture's dependency chain leads back to a fixture already in the active resolution set (depSet). Vitest builds a topological order of fixture instantiation; a cycle makes a correct order impossible. The message prints the chain so you can see which fixtures form the loop.

Solutions

  1. Break the cycle by extracting the shared dependency into a third fixture that both depend on.
  2. Use the per-test override parent (`fixture.parent`) correctly so overrides extend rather than replace.
  3. Review the printed chain `<-` list to identify the exact pair to sever.

Example fix

// before
const a = test.fn(async ({ b, use }) => use(b + 1))
const b = test.fn(async ({ a, use }) => use(a + 1)) // a <-> b cycle

// after
const base = test.fn(async ({ use }) => use(1))
const a = test.fn(async ({ base, use }) => use(base + 1))
const b = test.fn(async ({ base, use }) => use(base + 1))
Defensive patterns

Strategy: validation

Validate before calling

function detectCycle(fixtures: Map<string, string[]>): string[] | null {
  const visited = new Set<string>()
  const stack = new Set<string>()
  function dfs(name: string): string[] | null {
    if (stack.has(name)) return [...stack, name]
    if (visited.has(name)) return null
    visited.add(name); stack.add(name)
    for (const dep of fixtures.get(name) ?? []) {
      const cycle = dfs(dep)
      if (cycle) return cycle
    }
    stack.delete(name)
    return null
  }
  for (const name of fixtures.keys()) {
    const c = dfs(name)
    if (c) return c
  }
  return null
}

Try / catch

try {
  await runTest()
} catch (e) {
  if (/Circular fixture dependency/.test(e.message)) {
    // inspect the printed chain, refactor fixtures
  } else throw e
}

Prevention

When it happens

Trigger: Fixture A depends on B, B depends on C, C depends on A (transitive cycle); a fixture that depends on a name that resolves to itself via parent overrides without a base implementation. The parent branch only avoids the throw if a base fixture exists.

Common situations: Refactoring fixtures so two start mutual-depending; splitting one fixture into two that cross-reference; using `test.fn` per-test overrides that reintroduce a cycle.

Related errors


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

Appendix: source

Thrown at packages/vitest/src/runtime/runner/fixture.ts:573

  usedFixtures: TestFixtureItem[],
  registrations: FixtureRegistrations,
  depSet = new Set<TestFixtureItem>(),
  pendingFixtures: TestFixtureItem[] = [],
) {
  usedFixtures.forEach((fixture) => {
    if (pendingFixtures.includes(fixture)) {
      return
    }
    if (!isFixtureFunction(fixture.value) || !fixture.deps) {
      pendingFixtures.push(fixture)
      return
    }
    if (depSet.has(fixture)) {
      if (fixture.parent) {
        fixture = fixture.parent
      }
      else {
        throw new Error(
          `Circular fixture dependency detected: ${fixture.name} <- ${[...depSet]
            .reverse()
            .map(d => d.name)
            .join(' <- ')}`,
        )
      }
    }

    depSet.add(fixture)
    resolveDeps(
      Array.from(fixture.deps, n => n === fixture.name ? fixture.parent : registrations.get(n)).filter(n => !!n),
      registrations,
      depSet,
      pendingFixtures,
    )
    pendingFixtures.push(fixture)
    depSet.clear()
  })

View on GitHub (pinned to 1fa9837ec2)