vitest-dev/vitest · error · Error

onCleanup can only be called once per fixture. Define separa

Error message

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

What it means

The builder-pattern fixture syntax (`test.extend('name', fn)` where `fn` receives `{ onCleanup }`) wraps the function so that `onCleanup` registers a single teardown callback (suite.ts:881-891). If the fixture function calls `onCleanup` more than once, the second call throws this error. The wrapper only stores one cleanup; multiple cleanups require separate fixtures.

Source

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

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

Solutions

  1. Combine all cleanup logic into a single function passed to one `onCleanup` call.
  2. Define separate builder fixtures (each with its own onCleanup) for independent resources.
  3. If multiple teardowns are needed, use the classic `({ }, use) => { ...; await use(value); teardown() }` fixture form which can run arbitrary cleanup after `use`.

Example fix

// before
test.extend('db', (ctx, { onCleanup }) => {
  const a = openA()
  onCleanup(() => a.close())
  const b = openB()
  onCleanup(() => b.close()) // throws
  return { a, b }
})
// after
test.extend('db', (ctx, { onCleanup }) => {
  const a = openA()
  const b = openB()
  onCleanup(() => { a.close(); b.close() })
  return { a, b }
})
Defensive patterns

Strategy: validation

Validate before calling

// Track onCleanup calls in your builder fixture during development.
function makeFixture(fn: (ctx: any, h: { onCleanup: (f: () => void) => void }) => any) {
  let count = 0
  return (ctx: any, { onCleanup }: any) => fn(ctx, {
    onCleanup: (f: () => void) => {
      if (count++) throw new Error('onCleanup called twice')
      onCleanup(f)
    },
  })
}

Prevention

When it happens

Trigger: Using `test.extend('myFixture', (ctx, { onCleanup }) => { onCleanup(fn1); onCleanup(fn2); return value })` — calling onCleanup twice in the same builder fixture.

Common situations: Accumulating multiple teardowns in a loop; refactoring a fixture to add a second cleanup without splitting fixtures; misunderstanding that onCleanup is single-registration.

Related errors


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