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

  1. Move the `vi.mock`/`vi.hoisted` call to the top level of the test file (module scope).
  2. If you need conditional or runtime-dependent mocking inside a scope, use `vi.doMock`/`vi.doUnmock` instead — these are not hoisted.
  3. Use `vi.hoisted` at the top level to share variables between the factory and the test when hoisting reorders execution.
  4. 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

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


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 string

View on GitHub (pinned to 1fa9837ec2)