Solving the 'Cannot Access Before Initialization' Error in Vitest Mocks
Vitest hoists vi.mock calls, which can cause 'cannot access before initialization' errors. Learn how to use vi.hoisted to safely define mock data and functions.
23 Jun 2026, 05:57 UTC

The Problem: A ReferenceError in Your Test
When you write a Vitest test that mocks an external module, you might place the vi.mock call anywhere in the file. However, if the mock factory references a helper constant declared after the imports, Vitest will throw a ReferenceError at runtime:
import { getUsers } from "./service";
const mockData = { id: 1, name: "Alice" }; // Declared after imports
vi.mock("./api", () => ({
fetchUsers: vi.fn(() => Promise.resolve([mockData]))
})); // ← throws ReferenceError
Running this with npx vitest produces an error similar to: ReferenceError: Cannot access 'mockData' before initialization.
The culprit is Vitest’s transformation step: every vi.mock call is moved (hoisted) to the very top of the module before any other code runs. This ensures that the module under test receives the mocked version regardless of where you placed the call in your source file.
Why Hoisting Is Deliberate
In Vitest, the mock registration phase happens before the module’s imports are resolved. The engine transforms the file so that all vi.mock calls are executed first. This deterministic ordering prevents a race condition where the real module is imported before the mock is registered, which would render the mock ineffective.
Because of this hoisting, the textual order of your file does not match the execution order. Any reference to a variable defined in the module scope (like a const or let) becomes a runtime error because the mock factory is executing before those variables are initialized.
The Escape Hatch: vi.hoisted
Vitest provides vi.hoisted(fn) to create values that are available during the hoisted phase. The function you pass to vi.hoisted runs at the same time as the vi.mock calls, and its return value can be safely closed over inside the mock factory.
This allows you to define mock functions or fixture data that are initialized before the imports, solving the initialization error.
Worked Example: Mocking an API Dependency
Consider a service that imports fetchUsers from an api module and processes the results:
// service.ts
import { fetchUsers } from "./api";
export async function getUsers() {
const users = await fetchUsers();
return users.map(u => ({ ...u, fullName: u.name }));
}
To test this without hitting a real network, use vi.hoisted to define the mock function before the service is imported:
// service.test.ts
import { getUsers } from "./service";
// 1. Create the mock function in a hoisted block
const { mockFetchUsers } = vi.hoisted(() => ({
mockFetchUsers: vi.fn(),
}));
// 2. Register the mock for the api module
vi.mock("./api", () => ({
fetchUsers: mockFetchUsers
}));
它("maps users correctly", async () => {
// 3. Provide a mock implementation inside the test
mockFetchUsers.mockResolvedValueOnce([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
const result = await getUsers();
expect(result).toEqual([
{ id: 1, name: "Alice", fullName: "Alice" },
{ id: 2, name: "Bob", fullName: "Bob" },
]);
expect(mockFetchUsers).toHaveBeenCalledTimes(1);
});
Alternatives and Trade-offs
vi.spyOn
If you only need to override a single export of a module, you can import the real module and spy on the function. This avoids hoisting entirely because it happens during the normal execution flow.
Trade-off: Requires the real module to be importable and may trigger side effects during the initial import.
Manual Dependency Injection (DI)
Design your code so that the dependency is passed as an argument, e.g., getUsers(apiClient). You can then pass a simple stub during tests.
Trade-off: Requires refactoring production code to accommodate the testability pattern.
Partial Mocking with vi.importActual
When you need most of the module but want to override one export, use the factory's ability to import the original module:
vi.mock("./api", async () => {
const original = await vi.importActual("./api");
return { ...original, fetchUsers: vi.fn() };
});
Trade-off: Still subject to hoisting; the factory must remain self-contained or use vi.hoisted for external references.
Limitations of Hoisting
- Cognitive Load: Reading order no longer equals execution order, which can confuse developers unfamiliar with the Vitest transformation process.
- Scope Constraints: Mock factories cannot reference any variable in the file unless that variable is also wrapped in
vi.hoisted. - State Management: While each test file has its own module registry, you must use
vi.clearAllMocks()orvi.resetAllMocks()to prevent state leakage between individualitblocks within the same file.
Actionable Takeaways
- Standardize Placement: Place all
vi.mockcalls at the top of your test files to make the hoisting explicit to other developers. - Use Hoisted Fixtures: Whenever a mock factory needs a variable from the test file, wrap that variable in
vi.hoisted. - Prefer Spies for Simplicity: Use
vi.spyOnwhen you only need to change one method and the module has no dangerous side effects upon import. - Verify in CI: Run
npx vitest runin your pipeline to ensure that hoisting behavior remains consistent across different environments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.