vitest-dev/vitest · error · Error

Circular fixture dependency detected: ${fixture.name} <- ${[

Error message

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

What it means

Vitest resolves fixture dependencies by walking the dependency graph in `resolveDeps` (fixture.ts:554). If fixture A depends on B which depends back on A (directly or transitively) and there is no `parent` base implementation to break the cycle, the resolver detects the revisit and throws this error with the full cycle chain formatted as `A <- B <- A`.

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 d568f8ce37)

Solutions

  1. Break the cycle by introducing a shared base fixture that both depend on instead of each other.
  2. Use `test.extend` parent/base implementation: redefining a fixture by the same name creates a `parent` that breaks the self-dependency.
  3. Restructure so dependencies flow one direction (leaf fixtures have no fixture deps).

Example fix

// before: a depends on b, b depends on a
test.extend({
  a: ({ b }, use) => use(b + 1),
  b: ({ a }, use) => use(a + 1),
})
// after: shared base
test.extend({
  base: ({}, use) => use(0),
  a: ({ base }, use) => use(base + 1),
  b: ({ base }, use) => use(base + 1),
})
Defensive patterns

Strategy: validation

Validate before calling

// Topologically validate fixture dependencies before extend.
function hasCycle(defs: Record<string, string[]>): boolean {
  const visited = new Map<string, 'visiting' | 'done'>()
  function dfs(n: string): boolean {
    const s = visited.get(n)
    if (s === 'visiting') return true
    if (s === 'done') return false
    visited.set(n, 'visiting')
    for (const d of defs[n] ?? []) if (dfs(d)) return true
    visited.set(n, 'done')
    return false
  }
  return Object.keys(defs).some(dfs)
}

Prevention

When it happens

Trigger: Defining `a: ({ b }, use) => use(...)` and `b: ({ a }, use) => use(...)`; any transitive cycle across three or more fixtures; overriding a fixture to depend on one that ultimately depends on the override target.

Common situations: Splitting a monolithic fixture into two that reference each other; overriding fixtures in nested suites creating a cycle; refactoring fixtures without checking the dependency direction.

Related errors


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