vitest-dev/vitest · error · TypeError
Cannot spy on export
Error message
Cannot spy on export "${String(key)}". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/mocking/modules#mocking-a-module What it means
vi.spyOn attempts to redefine a property on the target object. ESM module namespace objects have non-configurable exports by spec, so redefinition throws a TypeError that Vitest catches and re-wraps into this actionable message. The library cannot patch live ESM namespace bindings; mocking must go through vi.mock at the module level instead.
Solutions
- Use vi.mock('module-name', () => ({ myFunc: vi.fn() })) to replace the module before it is imported.
- Refactor the code under test to accept the function as a parameter (dependency injection) so you can pass a spy directly.
- If the module has a default export that is an object, spy on that object's method instead of the namespace binding.
Example fix
// before — fails because ESM namespace is non-configurable:
import * as mod from './mod'
vi.spyOn(mod, 'doThing')
// after — mock at the module level:
vi.mock('./mod', () => ({ doThing: vi.fn() }))
import { doThing } from './mod' Defensive patterns
Strategy: type-guard
Validate before calling
import { isModuleNamespaceObject } from 'util/types'
// before spying, check if the target is an ESM namespace:
if (isModuleNamespaceObject(target)) {
// use vi.mock instead of vi.spyOn
} Type guard
function isESMNamespace(obj: unknown): boolean {
return typeof obj === 'object' && obj !== null
&& Object.prototype.toString.call(obj) === '[object Module]'
} Try / catch
try {
vi.spyOn(mod, 'fn')
} catch (e) {
if (e instanceof TypeError && e.message.includes('Module namespace is not configurable')) {
// fall back to vi.mock at module level
vi.mock('mod', () => ({ fn: vi.fn() }))
} else { throw e }
} Prevention
- Prefer vi.mock for ESM modules over vi.spyOn on namespace exports.
- Design code with dependency injection so functions can be spied without touching module bindings.
- Check whether a module is CJS or ESM before attempting spyOn on its exports.
When it happens
Trigger: Calling vi.spyOn on an imported ESM module's named export directly, e.g. `vi.spyOn(myModule, 'myFunc')` where myModule is a namespace object from a real ESM import (not a CJS interop).
Common situations: Migrating from Jest where spyOn on module exports worked via CJS; importing a pure-ESM dependency and trying to spy on its functions at runtime; testing code that wasn't designed with dependency injection.
Related errors
- Benchmark provider loaded from
- Cannot import " ": its vm context was torn down.
- Custom reporter loaded from
- Invalid data URI
- Invalid data URI encoding
AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11).
Data as JSON: /api/errors/e714ca69d975a0d6.
Report an issue: GitHub.
Appendix: source
Thrown at packages/spy/src/index.ts:436
})
try {
reassign(
ssr
? () => mock
: mock,
)
}
catch (error) {
if (
error instanceof TypeError
&& Symbol.toStringTag
&& (object as any)[Symbol.toStringTag] === 'Module'
&& (error.message.includes('Cannot redefine property')
|| error.message.includes('Cannot replace module namespace')
|| error.message.includes('can\'t redefine non-configurable property'))
) {
throw new TypeError(
`Cannot spy on export "${String(key)}". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/mocking/modules#mocking-a-module`,
{ cause: error },
)
}
throw error
}
return mock
}
function getDescriptor(obj: any, method: string | symbol | number): [any, PropertyDescriptor] | undefined {
const objDescriptor = Object.getOwnPropertyDescriptor(obj, method)
if (objDescriptor) {
return [obj, objDescriptor]
}
let currentProto = Object.getPrototypeOf(obj)
while (currentProto !== null) {View on GitHub (pinned to 1fa9837ec2)