Why the call throws instead of just warning
vi.mock(), vi.unmock(), and vi.hoisted() don’t run where they’re written. Vitest’s compiler transform lifts them to the very top of the file at parse time, before any other module-level code, regardless of how deeply they’re nested in the source. That’s what makes them useful for mocking a module before your own imports even resolve.
The problem is when the code around a hoisted call implies an order that isn’t the order it actually runs in. Vitest’s own migration guide states the change plainly, in its “Hoisted Mocking Calls” section:
vi.mock,vi.unmock, andvi.hoistedare hoisted to the top of the file and run before any surrounding code. Calling them inside a function, block, ordescribe/testcallback previously only logged a warning. Vitest 5.0 now throws, because the call does not execute where it is written.
Vitest 4 tolerated the mismatch and just printed a warning. Vitest 5.0 stops tolerating it. The reported error names the exact offending call and its location, quoted here from the guide’s own example:
1 call in "calculator.test.ts" was defined outside of the module's top level scope:
- vi.mock("./calculator") at calculator.test.ts:2:3
Although it appears nested, it will be hoisted and executed before anything
in this file. Move it to the top level to reflect its actual execution order.This is a real behavior change, not a lint rule
A test suite that ran clean under Vitest 4 with a vi.mock() nested inside a
describe block will fail outright after upgrading to Vitest 5, with no code
change on your side. The mock call itself was never broken. What changed is
that Vitest now refuses to let its written position disagree with its real
execution order.
Fix it: move the call to module top level
The guide’s own example shows the pattern directly. A vi.mock() call sitting inside a describe block gets hoisted anyway, so writing it there is misleading even before Vitest 5 makes it an error.
import { describe, expect, it, vi } from "vitest";
describe("calculator", () => {
vi.mock("./calculator"); // BROKEN: nested, throws in Vitest 5
// ...tests
});import { describe, expect, it, vi } from "vitest";
vi.mock("./calculator"); // FIXED: written at module top level, matches hoisted order
describe("calculator", () => {
// ...tests
});The fix is mechanical: pull every vi.mock(), vi.unmock(), and vi.hoisted() call out to the top of the file, outside any describe or test callback. Nothing about the mock’s behavior changes. Only its written position moves to match where it already ran.
When you actually need a mock set up at runtime
Sometimes the whole point is deciding what to mock inside a test, based on something you only know at runtime, not at parse time. Hoisting a call there doesn’t just look wrong, it’s the wrong tool. Vitest’s migration guide is explicit that the hoisted variants have a non-hoisted counterpart built for exactly this:
The dynamic variants
vi.doMockandvi.doUnmockare not hoisted and may still be called anywhere.
import { describe, expect, it, vi } from "vitest";
describe("calculator", () => {
it("uses a mocked add() only for this test", async () => {
vi.doMock("./calculator", () => ({ add: () => 42 })); // OK: not hoisted, runs in place
const { add } = await import("./calculator");
expect(add(1, 2)).toBe(42);
});
});vi.doMock() and vi.doUnmock() take effect only for imports that happen after they run, so pair them with a dynamic import() inside the test rather than a static top-of-file import. That’s a real difference from vi.mock(), not a cosmetic one. If your suite genuinely needs per-test mock variation, this is the supported path, not a workaround.
Confirmed version range
This is documented in Vitest’s own current migration guide as an intentional Vitest 5.0 change, in the section covering hoisted mocking calls. This repository’s own vitest.config.ts is pinned to vitest@^3.0.0, so this behavior wasn’t independently reproduced against this project’s build; every claim above is sourced directly from Vitest’s migration guide, not a local repro. Check the vi.hoisted API reference if you’re touching hoisted setup logic beyond vi.mock/vi.unmock, since the same top-level rule applies there too.
If your suite also leans on clearMocks, see Fix Vitest 5 clearMocks Breaking Mock State for the other Vitest 5 mocking default that changes silently on upgrade. More fixes like this one are in the Guides & Fixes archive, and the full list of Vitest posts is tagged under vitest.







