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
- Consolidate multiple teardowns into a single onCleanup that runs them in sequence.
- Split into multiple fixtures if resources have independent lifecycles.
- 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
- Register a single onCleanup that runs all teardowns in sequence.
- Split into multiple fixtures if resources have independent lifecycles.
- Consider the classic use-style fixture which handles one teardown naturally.
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 anyView on GitHub (pinned to 1fa9837ec2)