vitest-dev/vitest · error · Error

Mock ${id} wasn't registered. This is probably a Vitest erro

Error message

Mock ${id} wasn't registered. This is probably a Vitest error. Please, open a new issue with reproduction.

What it means

`getFactoryModule` (`nativeModuleMocker.ts:190`) is called by the native loader to resolve a manual mock's factory; it looks up the mock in the registry by id and requires it be type `'manual'`. If absent (or wrong type), it throws, explicitly noting this is likely a Vitest internal bug — the loader shouldn't be invoked for an unregistered mock. Indicates a state desync between registration and load.

Source

Thrown at packages/vitest/src/runtime/moduleRunner/nativeModuleMocker.ts:190

    this.processedModules.set(id, (this.processedModules.get(id) ?? 0) + 1)
    // the module is mocked and requested a second time, let's resolve
    // the factory function that will redefine the exports later
    if (this.originalModulePromises.has(id)) {
      const factoryPromise = this.factoryPromises.get(id)
      this.originalModulePromises.get(id)?.resolve({ __factoryPromise: factoryPromise })
    }
  }

  private originalModulePromises = new Map<string, DeferPromise<any>>()
  private factoryPromises = new Map<string, Promise<any>>()

  // potential performance improvement:
  // store by URL, not ids, no need to call url.*to* methods and normalizeModuleId
  public getFactoryModule(id: string): any {
    const registry = this.getMockerRegistry()
    const mock = registry.getById(id)
    if (!mock || mock.type !== 'manual') {
      throw new Error(`Mock ${id} wasn't registered. This is probably a Vitest error. Please, open a new issue with reproduction.`)
    }

    const mockResult = mock.resolve()
    if (mockResult instanceof Promise) {
      // to avoid circular dependency, we resolve this function as {__factoryPromise} in `checkCircularManualMock`
      // when it's requested the second time. then the exports are exposed as `undefined`,
      // but later redefined when the promise is actually resolved
      const promise = createDefer()
      promise.finally(() => {
        this.originalModulePromises.delete(id)
      })
      mockResult.then(promise.resolve, promise.reject).finally(() => {
        this.factoryPromises.delete(id)
      })
      this.factoryPromises.set(id, mockResult)
      this.originalModulePromises.set(id, promise)
      // Node.js on windows processes all the files first, and then runs them
      // unlike Node.js logic on Mac and Unix where it also runs the code while evaluating

View on GitHub (pinned to d568f8ce37)

Solutions

  1. Ensure all `vi.mock(...)` calls are at the top level of the test file (they're hoisted automatically only there).
  2. Avoid interleaving `vi.doUnmock`/`vi.unmock` with in-flight dynamic imports of the same module.
  3. If the setup looks correct, file a Vitest issue with a minimal reproduction (the message itself requests this).
Defensive patterns

Strategy: try-catch

Validate before calling

// Keep all vi.mock calls hoisted at the top level of the file.
// Avoid vi.mock inside conditionals, callbacks, or dynamic branches.
vi.mock('mod', () => ({ ok: true })) // top-level, hoisted

test('uses mod', async () => { /* ... */ })

Try / catch

try {
  await import('mod')
} catch (e) {
  if (e instanceof Error && /Mock .* wasn't registered/i.test(e.message)) {
    // file a Vitest issue; meanwhile check vi.mock hoisting/unmock ordering
  } else throw e
}

Prevention

When it happens

Trigger: Internal state desync: a `vi.doUnmock`/`vi.unmock` clearing the registry mid-load; a race in module loading; `vi.mock` calls not hoisted correctly (placed inside conditionals/callbacks) so registration is missed.

Common situations: `vi.mock` written inside an `if` or callback (not hoisted to top); mock reset/unmock interleaved with a still-pending dynamic import; a genuine Vitest bug.

Related errors


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