vitest-dev/vitest · error · AroundHookMultipleCallsError

The ` ` callback was called multiple times in the ` ` hook…

Error message

The `${callbackName}` callback was called multiple times in the `${hookName}` hook. The callback can only be called once per hook.

What it means

AroundHookMultipleCallsError thrown when the aroundEach/aroundAll callback (runTest/use) is invoked more than once in a single hook invocation. Around hooks wrap the test/suite exactly once: the first call runs the inner content, and a second call would re-enter already-run hooks. Vitest tracks a useCalled flag and raises this on the second invocation.

Solutions

  1. Ensure runTest is called exactly once along every code path through the hook.
  2. Restructure retry logic to live inside the test body, not by re-invoking runTest.
  3. If you need setup+teardown without wrapping, use beforeEach/afterEach instead of aroundEach.

Example fix

// before
aroundEach(async (runTest) => {
  await runTest()
  await runTest() // throws
})

// after
aroundEach(async (runTest) => {
  await setup()
  await runTest()
  await teardown()
})
Defensive patterns

Strategy: validation

Validate before calling

function aroundWrapper(setup: () => Promise<void>, teardown: () => Promise<void>) {
  return async (runTest: () => Promise<void>) => {
    await setup()
    await runTest() // called exactly once
    await teardown()
  }
}

Try / catch

try {
  await runWithAroundHook()
} catch (e) {
  if (e instanceof AroundHookMultipleCallsError) {
    // audit hook for double runTest invocation
  } else throw e
}

Prevention

When it happens

Trigger: Calling `runTest()` twice in the same aroundEach body; calling runTest in a loop; calling runTest from both a try and a finally block; calling runTest after an early await resolved.

Common situations: Copy-paste error; retry logic mistakenly wrapping runTest; conditional that calls runTest in two branches without early return.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/3ad5a6cdfd46e937. Report an issue: GitHub.

Appendix: source

Thrown at packages/vitest/src/runtime/runner/run.ts:352

    // Promise that resolves when hook completes
    let resolveHookComplete!: () => void
    let rejectHookComplete!: (error: Error) => void
    const hookCompletePromise = new Promise<void>((resolve, reject) => {
      resolveHookComplete = resolve
      rejectHookComplete = reject
    })

    const use = async () => {
      // shouldn't continue to next (runTest/Suite or inner aroundEach/All) when aroundEach/All setup timed out.
      if (setupTimeout.isTimedOut()) {
        // we can throw any error to bail out.
        // this error is not seen by end users since `runNextHook` already rejected with timeout error
        // and this error is caught by `rejectHookComplete`.
        throw new Error('__VITEST_INTERNAL_AROUND_HOOK_ABORT__')
      }

      if (useCalled) {
        throw new AroundHookMultipleCallsError(
          `The \`${callbackName}\` callback was called multiple times in the \`${hookName}\` hook. `
          + `The callback can only be called once per hook.`,
        )
      }
      useCalled = true
      resolveUseCalled()

      // Setup phase completed - clear setup timer
      setupTimeout.clear()
      setupLimitConcurrencyRelease?.()

      // Run inner hooks - don't time this against our teardown timeout
      await runNextHook(index + 1).catch(e => hookErrors.push(e))

      teardownLimitConcurrencyRelease = await limitMaxConcurrency.acquire()

      // Start teardown timer after inner hooks complete - only times this hook's teardown code
      teardownTimeout = createTimeoutPromise(timeout, 'teardown', stackTraceError)

View on GitHub (pinned to 1fa9837ec2)