Code that depends on time cannot be tested with real clocks: a test for debounce, an interval or a timeout will start to depend on how fast the machine is and become flaky. Vitest gives control over time via vi.useFakeTimers - it replaces timers and the system clock with controlled ones. From that point time stops being an external circumstance and becomes an input to the test that you drive explicitly.
The scheme is simple: in beforeEach you enable fake timers and pin the moment via vi.setSystemTime, and in afterEach you return the real ones via vi.useRealTimers. Inside the test you do not wait for time but advance it by hand - vi.advanceTimersByTime(300) winds exactly the needed interval instantly. So a test checking a 300 ms delay runs in microseconds and always the same way.
A telling example is debounce. The function is called twice in a row, then time is wound forward by the delay length and you check that the real handler fired exactly once and with the last argument. Without fake timers you would have to wait a real 300 ms and hope the scheduler made it; with them the scheduler's behavior is checked precisely and in one step.
It is important to separate domain time from scheduler time. For dates in data - created_at, a deadline, an expiry - it is better to inject a Clock as an explicit dependency and pass a fixed value in the test rather than replace the global Date. Fake timers are left for scheduler behavior: debounce, throttle, intervals, retry backoff. Then each tool is responsible for its part rather than replacing all of time at once.
Recursive timers are a separate trap. If a timer schedules a new timer when it fires, vi.runAllTimers() will go into an infinite loop trying to drain a queue that refills itself. In such cases you advance time in steps via advanceTimersByTime or run only what is already scheduled via runOnlyPendingTimers, without trying to page the queue to the end.
Replacement covers setTimeout, setInterval, the system clock and, if wanted, microtasks, process.nextTick and requestAnimationFrame. The set can be narrowed with the toFake option by passing a list of only the timers the test needs and leaving everything else real. This helps when replacing all timers at once breaks a third-party library that relies on the real scheduler: you then replace only what pertains to the behavior under test and leave the other machinery alone.
The typical failure is waiting for a real delay via sleep instead of fake timers. It both slows the suite and does not remove non-determinism: on a loaded CI delays drift. The second failure is forgetting vi.useRealTimers in afterEach: fake time leaks into the next tests, and a test entirely unrelated to timers suddenly hangs or sees a frozen date.
The bottom line is simple: fake timers turn time into a controlled input of the test. Instead of waiting, you move the clock explicitly to the point whose behavior you check - to the moment after debounce, to the next interval tick, to the fired timeout. Time becomes as much an argument of the scenario as the input data and stops being a source of random failures.
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-07-26T10:00:00Z'))
})
afterEach(() => vi.useRealTimers())
test('fires search after a 300 ms debounce', () => {
const search = vi.fn()
const debounced = debounce(search, 300)
debounced('je')
debounced('vitest')
vi.advanceTimersByTime(300)
expect(search).toHaveBeenCalledTimes(1)
expect(search).toHaveBeenCalledWith('vitest')
})