vitest-dev/vitest · error · Error

onCleanup can only be called once per fixture. Define…

Error message

onCleanup can only be called once per fixture. Define separate fixtures if you need multiple cleanup functions.

What it means

Thrown by the builder-pattern fixture wrapper when onCleanup is invoked more than once during a single fixture setup. The wrapper stores a single cleanup function and calls it after `use(value)` returns; a second registration would overwrite the first and leak resources. The pattern deliberately allows only one cleanup per fixture.

Solutions

  1. Consolidate multiple teardowns into a single onCleanup that runs them in sequence.
  2. Split into multiple fixtures if resources have independent lifecycles.
  3. Use the classic use-style fixture (`async ({ dep }, use) => { ...; await use(value); await cleanup() }`) which naturally handles one teardown.

Example fix

// before
test.fn(async (ctx, { onCleanup }) => {
  const a = createA(); onCleanup(() => a.dispose())
  const b = createB(); onCleanup(() => b.dispose()) // throws
  return { a, b }
})

// after
test.fn(async (ctx, { onCleanup }) => {
  const a = createA()
  const b = createB()
  onCleanup(() => { a.dispose(); b.dispose() })
  return { a, b }
})
Defensive patterns

Strategy: validation

Validate before calling

function makeCleanup(fns: Array<() => void>) {
  return () => fns.forEach(fn => fn())
}
// in builder fixture:
const cleanups: Array<() => void> = []
// ...allocate, push each teardown to cleanups
// onCleanup(makeCleanup(cleanups)) // single call

Try / catch

try {
  await use(value)
} catch (e) {
  if (/onCleanup can only be called once/.test(e.message)) {
    // consolidate cleanups and retry
  } else throw e
}

Prevention

When it happens

Trigger: Inside a builder-style fixture (`(ctx, { onCleanup }) => {...}`) calling `onCleanup(fn)` twice; looping over resources and calling onCleanup per iteration; calling onCleanup in two branches that both execute.

Common situations: Migrating from a use-style fixture to the builder pattern without consolidating cleanup; allocating multiple resources that each need teardown.

Related errors


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

Appendix: source

Thrown at packages/vitest/src/runtime/runner/suite.ts:892

        fixtureValue = {}
      }
      else {
        // (name, value) or (name, fn)
        fixtureOptions = undefined
        fixtureValue = optionsOrFn
      }
    }

    // Function value: wrap with onCleanup pattern
    if (typeof fixtureValue === 'function') {
      const builderFn = fixtureValue as (...args: any[]) => any

      // Wrap builder pattern function (returns value) to use() pattern
      const fixture = async (ctx: any, use: (value: any) => Promise<void>) => {
        let cleanup: (() => any) | undefined
        const onCleanup = (fn: () => any) => {
          if (cleanup !== undefined) {
            throw new Error(
              `onCleanup can only be called once per fixture. `
              + `Define separate fixtures if you need multiple cleanup functions.`,
            )
          }
          cleanup = fn
        }
        const value = await builderFn(ctx, { onCleanup })
        await use(value)
        if (cleanup) {
          await cleanup()
        }
      }
      configureProps(fixture, { original: builderFn })

      if (fixtureOptions) {
        return { [fixtureName]: [fixture, fixtureOptions] } as any
      }
      return { [fixtureName]: fixture } as any

View on GitHub (pinned to 1fa9837ec2)