vitest-dev/vitest · error · AroundHookMultipleCallsError
The `${callbackName}` callback was called multiple times in
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
`aroundEach` and `aroundAll` hooks receive a `runTest`/`runSuite` callback that must be called exactly once to proceed with the test/suite. In `callAroundHooks` (run.ts:342-356), the `use` closure tracks a `useCalled` flag; if `use`/`runTest`/`runSuite` is invoked a second time within the same hook, an `AroundHookMultipleCallsError` is thrown.
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 d568f8ce37)
Solutions
- Call `runTest`/`runSuite` exactly once, on the main code path.
- Ensure no try/catch/finally branch calls the callback more than once.
- If you need retry behavior, do it inside the test, not by re-invoking runTest.
Example fix
// before
aroundEach(async (runTest) => {
await setup()
await runTest()
await runTest() // second call throws
})
// after
aroundEach(async (runTest) => {
await setup()
await runTest()
await teardown()
}) Defensive patterns
Strategy: validation
Validate before calling
// Guard against double-call in your around hook with a local flag if logic is complex.
aroundEach(async (runTest) => {
let called = false
const runOnce = async () => {
if (called) throw new Error('runTest already called')
called = true
return runTest()
}
await setup()
await runOnce()
await teardown()
}) Prevention
- Call runTest/runSuite on a single unconditional code path.
- Avoid try/finally that could call it twice.
- Don't loop over runTest for retries.
When it happens
Trigger: Calling `runTest()` twice in an `aroundEach` hook; calling `runSuite()` in a loop inside `aroundAll`; awaiting `runTest()` and then calling it again conditionally; an accidental double-call due to a try/finally that calls it in both paths.
Common situations: Wrapping runTest in conditional logic where both branches call it; retry logic that re-invokes runTest; misunderstanding that runTest is single-shot.
Related errors
- The `${callbackName}` callback was not called in the `${hook
- Hook ${name}() can only be called inside a test
- Cannot call "onTestFailed" inside a test hook.
- Cannot call "onTestFinished" inside a test hook.
- Page "${sessionId}" not found in ${this.browserName} browser
AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03).
Data as JSON: /data/errors/3ad5a6cdfd46e937.json.
Report an issue: GitHub.