{"record":{"id":"e697a1e872c975a5","repo":"vitest-dev/vitest","slug":"cannot-call-ontestfinished-inside-a-test-hook","errorCode":null,"errorMessage":"Cannot call \"onTestFinished\" inside a test hook.","messagePattern":"Cannot call \"onTestFinished\" inside a test hook\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/runtime/runner/run.ts","lineNumber":142,"sourceCode":"  sequence: SequenceHooks,\n) {\n  if (sequence === 'stack') {\n    hooks = hooks.slice().reverse()\n  }\n\n  if (!hooks.length) {\n    return\n  }\n\n  const context = test.context as WriteableTestContext\n\n  const onTestFailed = test.context.onTestFailed\n  const onTestFinished = test.context.onTestFinished\n  context.onTestFailed = () => {\n    throw new Error(`Cannot call \"onTestFailed\" inside a test hook.`)\n  }\n  context.onTestFinished = () => {\n    throw new Error(`Cannot call \"onTestFinished\" inside a test hook.`)\n  }\n\n  if (sequence === 'parallel') {\n    try {\n      await Promise.all(hooks.map(fn => limitMaxConcurrency(() => fn(test.context))))\n    }\n    catch (e) {\n      failTask(test.result!, e, runner.config._diffOptions)\n    }\n  }\n  else {\n    for (const fn of hooks) {\n      try {\n        await limitMaxConcurrency(() => fn(test.context))\n      }\n      catch (e) {\n        failTask(test.result!, e, runner.config._diffOptions)\n      }","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/vitest-dev/vitest/blob/1fa9837ec26533512fdcad8baebf249771bd340a/packages/vitest/src/runtime/runner/run.ts#L124-L160","documentation":"Twin of the onTestFailed guard: while hooks run, context.onTestFinished is replaced with a throwing stub. onTestFinished registers a callback for the current test's completion, which is meaningless inside a hook that runs outside the per-test execution frame. The real onTestFinished is restored after the hooks finish.","triggerScenarios":"Calling `context.onTestFinished(fn)` from inside any beforeAll/afterAll/beforeEach/afterEach hook body, directly or through a helper.","commonSituations":"Resource-cleanup helpers that try to register onTestFinished regardless of call site; converting a test-body cleanup into a shared hook without removing the onTestFinished call.","solutions":["Register onTestFinished inside the test body.","Use afterEach for cleanup that must run after every test.","For per-test dynamic cleanup, pass the test context into your helper and call onTestFinished from the test."],"exampleFix":"// before\nbeforeEach(() => {\n  ctx.onTestFinished(() => stopServer()) // throws\n})\n\n// after\ntest('x', () => {\n  ctx.onTestFinished(() => stopServer())\n})","handlingStrategy":"validation","validationCode":"function safeOnTestFinished(ctx, fn) {\n  if (!ctx.task?.result || ctx.task.result.state !== 'run') return\n  ctx.onTestFinished(fn)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Register onTestFinished inside the test body, not in hooks.","Use afterEach for deterministic post-test cleanup.","Pass test context explicitly to shared helpers."],"tags":["hooks","test-context","lifecycle"],"backgroundTag":null,"analyzedSha":"1fa9837ec26533512fdcad8baebf249771bd340a","analyzedAt":"2026-08-11T16:11:39.638Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}