{"record":{"id":"1e7573a3db72fcca","repo":"vitest-dev/vitest","slug":"cannot-call-ontestfailed-inside-a-test-hook","errorCode":null,"errorMessage":"Cannot call \"onTestFailed\" inside a test hook.","messagePattern":"Cannot call \"onTestFailed\" inside a test hook\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/runtime/runner/run.ts","lineNumber":139,"sourceCode":"  runner: VitestRunner,\n  test: Test,\n  hooks: ((context: TestContext) => Awaitable<void>)[],\n  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      }","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/vitest-dev/vitest/blob/1fa9837ec26533512fdcad8baebf249771bd340a/packages/vitest/src/runtime/runner/run.ts#L121-L157","documentation":"While executing setup/teardown hooks (beforeEach/afterEach/beforeAll/afterAll), Vitest replaces context.onTestFailed with a function that throws this error. onTestFailed only makes sense for the test about to run or that just ran; within a hook there is no single test to attach the failure handler to. The original context.onTestFailed is restored after hooks complete.","triggerScenarios":"Calling `context.onTestFailed(fn)` or `this.onTestFailed(fn)` from inside a beforeEach/afterEach/beforeAll/afterAll callback; calling it transitively via a helper invoked from a hook.","commonSituations":"Shared helpers that register onTestFailed unconditionally; refactoring a test-body assertion into a hook; library code (e.g., custom matchers) that registers failure callbacks.","solutions":["Move the onTestFailed registration into the test body where it belongs.","If you need hook-scoped failure handling, capture failures via try/catch in the hook or use afterEach to inspect test.result.state.","Make helpers accept a flag so they skip onTestFailed registration when called from hooks."],"exampleFix":"// before\nbeforeEach(() => {\n  ctx.onTestFailed(() => log('failed')) // throws\n})\n\n// after\ntest('x', () => {\n  ctx.onTestFailed(() => log('failed'))\n})","handlingStrategy":"validation","validationCode":"function safeOnTestFailed(ctx, fn) {\n  // only call from a test body; detect hook context heuristically\n  if (!ctx.task?.result || ctx.task.result.state !== 'run') return\n  ctx.onTestFailed(fn)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never call context.onTestFailed from within before*/after* hooks.","Design helpers to accept an explicit 'inHook' flag to skip registration.","Keep failure-handler registration inside the test body."],"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"}