vitest-dev/vitest · error · Error

${hoistedNodes.size} call${plural ? 's' : ''} in "${relative

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:

${Array.from(hoistedNodes, (invalidNode) => `- ${getNodeName(getNodeCall(invalidNode))}${location}`).join('\n')}

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

What it means

Thrown at packages/mocker/src/node/hoistMocks.ts:507-527 when, after hoisting analysis, hoistedNodes still contains calls that are not at the module's top level (and import.meta.vitest was not used). vi.mock/unmock/hoisted are hoisted to the top of the file by the transformer regardless of where they are written; nesting them in a function/block is misleading because they actually run first. Vitest errors to force the code to reflect real execution order.

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 d568f8ce37)

Solutions

  1. Move the vi.mock/vi.unmock/vi.hoisted call to the top level of the test module.
  2. For conditionals or per-test mocks, use vi.doMock/vi.doUnmock (which are NOT hoisted and can live inside functions/tests).
  3. If reusing across files, keep mocks at the top level of each file or use a shared setup file.

Example fix

// before
function setup() {
  vi.mock('./logger')
}
it('logs', () => { setup(); /* ... */ })

// after (top-level, hoisted)
vi.mock('./logger')
it('logs', () => { /* ... */ })

// or use doMock for runtime-controlled mocking
it('logs', () => {
  vi.doMock('./logger')
  /* ... */
})
Defensive patterns

Strategy: validation

Validate before calling

// Static check: ensure vi.mock/unmock/hoisted calls are at top level.
// At review time, grep for these calls inside function/if/for blocks.
//
// Example guard helper that uses the non-hoisted API (doMock) when conditional:
function mockConditionally(path: string, enabled: boolean) {
  if (!enabled) return
  // vi.doMock is NOT hoisted, so it is safe inside a function
  vi.doMock(path)
}

Prevention

When it happens

Trigger: Placing vi.mock(...), vi.unmock(...), or vi.hoisted(...) inside a function body, an if/else block, a loop, a callback, or any non-top-level scope. The check at hoistMocks.ts:499-507 walks ast.body and removes top-level nodes; anything left over triggers the error.

Common situations: Conditionally mocking based on a flag: `if (cond) vi.mock('./x')`; wrapping mocks in a helper function for reuse; mocking inside beforeAll; attempting per-test mocks with vi.mock instead of vi.doMock.

Related errors


AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03). Data as JSON: /data/errors/6e05a49d4fc62897.json. Report an issue: GitHub.