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
- Ensure runTest is called exactly once along every code path through the hook.
- Restructure retry logic to live inside the test body, not by re-invoking runTest.
- 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
- Call runTest exactly once per aroundEach/aroundAll body.
- Avoid loops or branches that could invoke runTest twice.
- Use beforeEach/afterEach when you do not need to wrap the test.
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
- The ` ` callback was not called in the ` ` hook. Make sure…
- Cannot call "onTestFailed" inside a test hook.
- Cannot call "onTestFinished" inside a test hook.
- Hook () can only be called inside a test
- Browser is not initialized
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)