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 anyView on GitHub (pinned to d568f8ce37)
Solutions
- Combine all cleanup logic into a single function passed to one `onCleanup` call.
- Define separate builder fixtures (each with its own onCleanup) for independent resources.
- 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
- Call onCleanup at most once per builder fixture.
- Combine multiple teardowns into one cleanup function.
- Use the classic ({ }, use) fixture form for multi-step teardown.
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
- Cannot resolve user fixtures. See errors for more informatio
- Circular fixture dependency detected: ${fixture.name} <- ${[
AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03).
Data as JSON: /data/errors/073f84925bd92f8b.json.
Report an issue: GitHub.