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
- Break the cycle by extracting the shared dependency into a third fixture that both depend on.
- Use the per-test override parent (`fixture.parent`) correctly so overrides extend rather than replace.
- 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
- Extract shared dependencies into a base fixture rather than cross-referencing.
- Use per-test overrides via test.fn with parent correctly to extend, not replace.
- Visualize fixture deps when adding a new dependency.
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
- Cannot resolve user fixtures. See errors for more…
- onCleanup can only be called once per fixture. Define…
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)