{"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":885,"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":867,"sourceCodeEnd":903,"githubUrl":"https://github.com/vitest-dev/vitest/blob/d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09/packages/vitest/src/runtime/runner/suite.ts#L867-L903","documentation":"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.","triggerScenarios":"Using `test.extend('myFixture', (ctx, { onCleanup }) => { onCleanup(fn1); onCleanup(fn2); return value })` — calling onCleanup twice in the same builder fixture.","commonSituations":"Accumulating multiple teardowns in a loop; refactoring a fixture to add a second cleanup without splitting fixtures; misunderstanding that onCleanup is single-registration.","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`."],"exampleFix":"// before\ntest.extend('db', (ctx, { onCleanup }) => {\n  const a = openA()\n  onCleanup(() => a.close())\n  const b = openB()\n  onCleanup(() => b.close()) // throws\n  return { a, b }\n})\n// after\ntest.extend('db', (ctx, { onCleanup }) => {\n  const a = openA()\n  const b = openB()\n  onCleanup(() => { a.close(); b.close() })\n  return { a, b }\n})","handlingStrategy":"validation","validationCode":"// Track onCleanup calls in your builder fixture during development.\nfunction makeFixture(fn: (ctx: any, h: { onCleanup: (f: () => void) => void }) => any) {\n  let count = 0\n  return (ctx: any, { onCleanup }: any) => fn(ctx, {\n    onCleanup: (f: () => void) => {\n      if (count++) throw new Error('onCleanup called twice')\n      onCleanup(f)\n    },\n  })\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["fixtures","builder-pattern","oncleanup","extend"],"analyzedSha":"d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09","analyzedAt":"2026-08-03T20:23:56.861Z","schemaVersion":2}