{"record":{"id":"073f84925bd92f8b","repo":"vitest-dev/vitest","slug":"oncleanup-can-only-be-called-once-per-fixture-def","errorCode":null,"errorMessage":"onCleanup can only be called once per fixture. Define separate fixtures if you need multiple cleanup functions.","messagePattern":"onCleanup can only be called once per fixture\\. Define separate fixtures if you need multiple cleanup functions\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/runtime/runner/suite.ts","lineNumber":892,"sourceCode":"        fixtureValue = {}\n      }\n      else {\n        // (name, value) or (name, fn)\n        fixtureOptions = undefined\n        fixtureValue = optionsOrFn\n      }\n    }\n\n    // Function value: wrap with onCleanup pattern\n    if (typeof fixtureValue === 'function') {\n      const builderFn = fixtureValue as (...args: any[]) => any\n\n      // Wrap builder pattern function (returns value) to use() pattern\n      const fixture = async (ctx: any, use: (value: any) => Promise<void>) => {\n        let cleanup: (() => any) | undefined\n        const onCleanup = (fn: () => any) => {\n          if (cleanup !== undefined) {\n            throw new Error(\n              `onCleanup can only be called once per fixture. `\n              + `Define separate fixtures if you need multiple cleanup functions.`,\n            )\n          }\n          cleanup = fn\n        }\n        const value = await builderFn(ctx, { onCleanup })\n        await use(value)\n        if (cleanup) {\n          await cleanup()\n        }\n      }\n      configureProps(fixture, { original: builderFn })\n\n      if (fixtureOptions) {\n        return { [fixtureName]: [fixture, fixtureOptions] } as any\n      }\n      return { [fixtureName]: fixture } as any","sourceCodeStart":874,"sourceCodeEnd":910,"githubUrl":"https://github.com/vitest-dev/vitest/blob/1fa9837ec26533512fdcad8baebf249771bd340a/packages/vitest/src/runtime/runner/suite.ts#L874-L910","documentation":"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.","triggerScenarios":"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.","commonSituations":"Migrating from a use-style fixture to the builder pattern without consolidating cleanup; allocating multiple resources that each need teardown.","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."],"exampleFix":"// before\ntest.fn(async (ctx, { onCleanup }) => {\n  const a = createA(); onCleanup(() => a.dispose())\n  const b = createB(); onCleanup(() => b.dispose()) // throws\n  return { a, b }\n})\n\n// after\ntest.fn(async (ctx, { onCleanup }) => {\n  const a = createA()\n  const b = createB()\n  onCleanup(() => { a.dispose(); b.dispose() })\n  return { a, b }\n})","handlingStrategy":"validation","validationCode":"function makeCleanup(fns: Array<() => void>) {\n  return () => fns.forEach(fn => fn())\n}\n// in builder fixture:\nconst cleanups: Array<() => void> = []\n// ...allocate, push each teardown to cleanups\n// onCleanup(makeCleanup(cleanups)) // single call","typeGuard":null,"tryCatchPattern":"try {\n  await use(value)\n} catch (e) {\n  if (/onCleanup can only be called once/.test(e.message)) {\n    // consolidate cleanups and retry\n  } else throw e\n}","preventionTips":["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."],"tags":["fixtures","cleanup","builder-pattern"],"backgroundTag":null,"analyzedSha":"1fa9837ec26533512fdcad8baebf249771bd340a","analyzedAt":"2026-08-11T16:11:39.638Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}