vitest-dev/vitest · error · Error
call in " " defined outside of the module's top level scope
Error message
${hoistedNodes.size} call${plural ? 's' : ''} in "${relative(options.root || process.cwd(), id)}" ${plural ? 'were' : 'was'} defined outside of the module's top level scope: What it means
Thrown by `hoistMocks` after AST analysis if any `vi.mock`/`vi.hoisted` call node remains in the `hoistedNodes` set after removing top-level body nodes — meaning the call is lexically nested inside a function, block, conditional, or loop rather than at module top level. Because Vitest hoists these calls to the top of the file regardless of where they are written, a nested call would execute before the enclosing scope's other statements, which is surprising and error-prone, so Vitest rejects it with a detailed, located message.
Solutions
- Move the `vi.mock`/`vi.hoisted` call to the top level of the test file (module scope).
- If you need conditional or runtime-dependent mocking inside a scope, use `vi.doMock`/`vi.doUnmock` instead — these are not hoisted.
- Use `vi.hoisted` at the top level to share variables between the factory and the test when hoisting reorders execution.
- If the call is already at the top level, check for an editor/auto-formatter that wrapped it in an IIFE or block.
Example fix
// before — vi.mock nested inside a conditional
if (process.env.CI) {
vi.mock('./mod', () => ({ fn: vi.fn() }))
}
// after — top-level mock; use vi.hoisted for shared state
vi.mock('./mod', () => ({ fn: vi.fn() })) Defensive patterns
Strategy: validation
Validate before calling
// Lint test files for nested vi.mock/vi.hoisted calls before running. // Example ESLint rule logic: flag any vi.mock/vi.hoisted call whose AST depth is > 1 (not a direct child of Program).
Prevention
- Keep all vi.mock and vi.hoisted calls at module top level.
- Use vi.doMock/vi.doUnmock for runtime-dependent or nested mocking.
- Use vi.hoisted at the top level to share state between the factory and the test body.
When it happens
Trigger: Writing `vi.mock(...)` or `vi.hoisted(...)` inside a function body, `if` block, `try/catch`, loop, or any non-top-level scope. Note: `vi.doMock`/`vi.doUnmock` are intentionally NOT hoisted and are allowed inside nested scopes.
Common situations: Conditionally mocking inside `if` branches or helper functions; mocking inside `beforeEach`/`beforeAll`; wrapping `vi.mock` in a utility that tests call; copy-pasting a mock into a nested describe block's closure.
Related errors
- There are some problems in resolving the mocks API. You may…
- AssignmentPattern is not supported. Please open a new bug…
- automocking files with `export *` is not supported because…
- failed to parse
- MemberExpression is not supported. Please open a new bug…
AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11).
Data as JSON: /api/errors/49965f70bc1ab9ba.
Report an issue: GitHub.
Appendix: source
Thrown at packages/mocker/src/node/hoistMocks.ts:526
const locations = createIndexLocationsMap(code)
const map = options.getMap && new TraceMap(options.getMap() as any)
const plural = hoistedNodes.size > 1
const message = [
`${hoistedNodes.size} call${plural ? 's' : ''} in "${relative(options.root || process.cwd(), id)}" ${plural ? 'were' : 'was'} defined outside of the module's top level scope:`,
'',
...Array.from(hoistedNodes, (invalidNode) => {
const currentLocation = locations.get(invalidNode.start)
const originalLocation = map && currentLocation && originalPositionFor(map, currentLocation)
const location = originalLocation?.column != null && originalLocation?.line != null
? ` at ${relative(options.root || process.cwd(), id)}:${originalLocation.line}:${originalLocation.column + 1}`
: ''
return `- ${getNodeName(getNodeCall(invalidNode))}${location}`
}),
'',
`Although ${plural ? 'they appear nested, they' : 'it appears nested, it'} will be hoisted and executed before anything in this file. Move ${plural ? 'them' : 'it'} to the top level to reflect ${plural ? 'their' : 'its'} actual execution order.`,
'See: https://vitest.dev/guide/mocking/modules#how-it-works',
].join('\n')
throw new Error(message)
}
}
// hoist vi.mock/vi.hoisted
for (const node of arrayNodes) {
const end = getNodeTail(code, node)
// don't hoist into itself if it's already at the top
if (hoistIndex === end || hoistIndex === node.start) {
hoistIndex = end
}
else {
s.move(node.start, end, hoistIndex)
}
}
// hoist actual dynamic imports last so they are inserted after all hoisted mocks
for (const { node: importNode, id: importId } of imports) {
const source = importNode.source.value as stringView on GitHub (pinned to 1fa9837ec2)