PRACTICAL GUIDE / Vitest fake timers
Vitest Fake Timers for Deterministic JavaScript Tests
Vitest fake timers make debounce, retry, polling, and date tests deterministic. Learn timer setup, async advancement, bounded drains, and cleanup.
In this guide10 sections
- What Do Vitest Fake Timers Replace?
- When Should You Use Fake Timers Instead of Real Time?
- How Do You Set Up and Clean Up Vitest Fake Timers?
- How Do You Test a Debounce Boundary?
- When Do You Need Async Timer Advancement?
- Which Vitest Timer Control Should You Choose?
- Follow a Numbered Deterministic Timer Workflow
- Add the Pattern to This Repo's Vitest Setup
- Frequently Asked Questions About Vitest Fake Timers
- How do I enable fake timers in Vitest?
- What is the difference between synchronous and async advancement?
- When should I use runOnlyPendingTimers?
- Does vi.useFakeTimers mock Date?
- Why does a later test hang?
- Can Vitest control a real browser page clock?
- Conclusion: Control Time at One Clear Boundary
What you will learn
- What Do Vitest Fake Timers Replace?
- When Should You Use Fake Timers Instead of Real Time?
- How Do You Set Up and Clean Up Vitest Fake Timers?
- How Do You Test a Debounce Boundary?
Vitest fake timers replace selected clock and scheduling APIs so a test can create delayed work, advance virtual time, assert intermediate states, and restore native timers without waiting for wall-clock delays. Use synchronous controls for purely synchronous callbacks and async controls when a timer callback schedules promises or further asynchronous timers.
The repository already runs Vitest through vitest.config.ts with a node
environment and an include pattern of src/**/*.test.ts. Fake timers are not
currently configured there. This guide starts with local test ownership, then
shows an optional policy based on the existing defineConfig shape. Its cases
come from javascript-testing-fake-timers-quiz in
src/db/seed-data/challenges/expansion-js-ts.ts and are checked against the
current Vitest timer guide, vi
API, and fakeTimers
configuration.
What Do Vitest Fake Timers Replace?
vi.useFakeTimers() installs controlled replacements for timer and clock APIs
in the current test environment. The current Vitest API documents common
timers such as setTimeout, setInterval, their clear functions, immediate
APIs where available, and Date. Calls made after installation are controlled
until vi.useRealTimers() restores native implementations.
The order matters. A timeout created before fake timers are installed belongs to the real clock. Advancing the fake clock cannot make that real timer fire. Install the clock before rendering a component, calling a debounce wrapper, or constructing an object that schedules work.
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test('runs delayed work under the controlled clock', () => {
const task = vi.fn();
setTimeout(task, 500);
expect(task).not.toHaveBeenCalled();
vi.advanceTimersByTime(500);
expect(task).toHaveBeenCalledTimes(1);
});Fake timers do not mean "real time, but faster." The default model is a clock that moves only when the test tells it to move. That makes elapsed time an input controlled by the test. It also means any helper that expects wall-clock progress can stop if the test does not advance time.
Vitest's configuration can customize which APIs are faked. Current docs state
that nextTick and queueMicrotask are not included by default, though they
can be selected through configuration, with an environment caveat documented
for nextTick and fork pools. Do not assume every microtask source is controlled
because timeout APIs are controlled.
The controlled surface should be no wider than the test needs. Faking Date
and timeout APIs together is useful when code schedules work and records the
current instant. A test concerned only with a delay may not need to customize
the default set. A project that adds microtask APIs through toFake takes on
extra runner and pool behavior, so the configuration should include a focused
test proving the selected environment still starts, advances, and cleans up.
Third-party libraries may capture timer functions during module initialization.
If a module stores a reference to the real setTimeout before the fake clock
is installed, later replacement of the global function may not control that
stored reference. Install fake timers before importing such a module when the
runner and module system permit controlled import timing, or inject a scheduler
into the production code. Do not repeatedly advance the fake clock when the
real problem is a timer reference outside its reach.
Scheduler injection can make the boundary explicit:
- Production receives a default scheduler backed by native timers.
- Tests receive a small scheduler whose
delay,now, orschedulemethods can be controlled directly. - Vitest fake timers remain useful for code that legitimately uses global timer APIs.
Injection is not automatically better. It adds an application abstraction and should be used when the code has a meaningful clock dependency, not to avoid learning the runner API for one timeout.
The event loop still matters when callbacks create promises. JavaScript event-loop ordering in browser tests explains tasks and microtasks more broadly. The timer article's narrower question is which Vitest control moves the queue needed by the test.
When Should You Use Fake Timers Instead of Real Time?
Use fake timers when time passage is a controllable dependency and the contract can be expressed through scheduled boundaries. Common examples include:
- A search request fires only after a debounce interval.
- A retry occurs after a defined backoff.
- A poller performs one cycle at each interval.
- A cache or session expires at a known instant.
- A banner disappears after a timeout.
- A date-based rule activates at a calendar boundary.
- A scheduler queues work for the next tick or delayed interval.
These tests should assert behavior before and after the boundary. A debounce test that advances directly to the firing time and checks one call does not prove the callback waited. A stronger test checks no call just before the threshold and exactly one call at the threshold.
Do not use fake timers when the test's purpose is to measure real latency, verify interaction with an operating-system clock, exercise a real network timeout, or observe a browser page whose clock lives outside the Vitest environment. For browser-page control, use Playwright Clock fake time. For real timeout ownership, Playwright timeout budgeting shows a separate end-to-end concern.
Date logic deserves a boundary of its own. Fake timers can pin Date, but time
zones, locale formatting, daylight-saving transitions, and parsing rules still
need explicit inputs. Test dates and time zones without flaky
JavaScript covers
those domain choices, while debugging JavaScript date and time-zone test
failures addresses
environment-dependent failures.
Avoid treating fake timers as a general cure for flaky tests. If the system
waits for a database row, message, or browser state, virtual time may not
control the external dependency. Poll the observable state or replace the
dependency at a clear boundary. Playwright expect.poll and
toPass demonstrates state polling in
another runner.
How Do You Set Up and Clean Up Vitest Fake Timers?
Local setup makes ownership visible. A test that calls vi.useFakeTimers
should arrange cleanup that executes even when an assertion fails. A file-level
afterEach is a common choice because it restores real timers after every
case.
import { afterEach, describe, expect, test, vi } from 'vitest';
describe('session expiry', () => {
afterEach(() => {
vi.useRealTimers();
});
test('expires at the configured instant', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2027-01-01T00:00:00.000Z'));
const session = createSession({
expiresAt: new Date('2027-01-01T00:00:01.000Z'),
});
expect(session.isExpired()).toBe(false);
vi.setSystemTime(new Date('2027-01-01T00:00:01.000Z'));
expect(session.isExpired()).toBe(true);
});
});This example tests a function that reads Date. It does not claim that
setSystemTime automatically fires scheduled timers. Setting current system
time and advancing the timer queue are distinct operations. Use the operation
that corresponds to the production behavior.
Before restoring real timers, decide what should happen to pending work. The
current vi.useRealTimers documentation states that timers scheduled under the
fake clock are discarded when native timers are restored. That may be the
desired teardown, but it should not silently skip behavior the test was meant
to verify.
One cleanup pattern is to assert the pending count with vi.getTimerCount
before restoration. Another is to run only work that the contract requires and
then restore. Avoid globally draining every pending timer in cleanup because a
recursive poller can continually schedule new work, and cleanup should not add
untested side effects after assertions.
Fake timer state is shared within the test environment. Leaving it active can
make a later test's setTimeout, polling helper, or library wait stop moving.
That later test often receives the blame because it is where the timeout is
reported. Restore native timers at the boundary where they were replaced.
Cleanup order matters when a test also owns mocks or rendered components. Unmount or dispose code that might schedule final timers while the expected clock is still installed, assert or discard that work deliberately, and then restore native timers. Restoring first can cause teardown to create real timers that outlive the test. Draining blindly after unmount can execute callbacks the application intended to cancel. The correct order follows the component or service lifecycle under test.
When a test fails before it reaches its final assertion, afterEach still
provides the timer restoration boundary. A manual vi.useRealTimers at the end
of the test body can be skipped by a thrown assertion. Keep the hook even if
individual examples also restore timers for readability.
How Do You Test a Debounce Boundary?
A debounce contract has at least three observable properties: calls do not happen before the interval, the latest input is used when the interval ends, and repeated input resets or updates the scheduled work according to the implementation.
The repository's javascript-testing-fake-timers-quiz uses a 300 millisecond
boundary. The important technique is the split advance: move to 299
milliseconds, assert no call, then move one more millisecond and assert the
call.
import { afterEach, expect, test, vi } from 'vitest';
function debounce<T extends unknown[]>(
callback: (...args: T) => void,
delayMs: number,
) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: T) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => callback(...args), delayMs);
};
}
afterEach(() => {
vi.useRealTimers();
});
test('waits for the complete debounce interval', () => {
vi.useFakeTimers();
const search = vi.fn();
const debouncedSearch = debounce(search, 300);
debouncedSearch('vit');
debouncedSearch('vitest');
vi.advanceTimersByTime(299);
expect(search).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(search).toHaveBeenCalledTimes(1);
expect(search).toHaveBeenCalledWith('vitest');
});This is a real executable unit test. It proves the pre-boundary state and final argument rather than only checking that a callback eventually happened. It also installs fake timers before calling the wrapper, so the created timeout belongs to the controlled clock.
If the implementation schedules a promise inside the timeout, the synchronous advance may fire the timeout callback but leave promise reactions pending. That is where the async timer controls become necessary.
Testing Library or component frameworks may have their own update-flushing requirements. Fake timers do not replace framework-specific rendering boundaries. Keep the clock advance and the UI update owner explicit, and use current framework guidance instead of adding repeated advances until the test passes.
When Do You Need Async Timer Advancement?
Use an async timer method when firing a timer is only the first step in an asynchronous chain. A retry helper might await a timeout, call an async operation, inspect the rejection, and schedule another timeout. The test must let both timer and promise work progress.
Vitest documents vi.advanceTimersByTimeAsync as the asynchronous counterpart
to advanceTimersByTime. It also provides async forms of all-timer,
pending-timer, and next-timer controls. Await these methods so the test runner
owns the timer advancement.
import { afterEach, expect, test, vi } from 'vitest';
const delay = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
async function retryOnce<T>(operation: () => Promise<T>): Promise<T> {
try {
return await operation();
} catch {
await delay(1_000);
return operation();
}
}
afterEach(() => {
vi.useRealTimers();
});
test('retries after the controlled delay', async () => {
vi.useFakeTimers();
const operation = vi
.fn<() => Promise<string>>()
.mockRejectedValueOnce(new Error('temporary'))
.mockResolvedValueOnce('ready');
const result = retryOnce(operation);
expect(operation).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1_000);
await expect(result).resolves.toBe('ready');
expect(operation).toHaveBeenCalledTimes(2);
});The test stores the result promise before advancing time, then awaits both the clock movement and the result. Calling the synchronous advance can leave the continuation waiting in promise work, depending on the chain. Adding another arbitrary advance does not describe the contract; using the documented async operation does.
Async timer control does not adopt promises the test creates and forgets to await. Promise ownership remains necessary. Debugging unhandled promise rejections in tests covers the escaped-promise symptom, and async orchestration architecture for JavaScript test runners explains the wider scheduling model.
Which Vitest Timer Control Should You Choose?
Choose the narrowest operation that represents the behavior. A bounded advance usually communicates more than draining every timer because it preserves the meaning of elapsed time.
| Vitest API | Queue behavior | Async timer work | Recursive scheduling risk | Best use |
|---|---|---|---|---|
advanceTimersByTime(ms) | Runs timers within a virtual interval | Synchronous path | Bounded by interval, but intervals can fire repeatedly | Debounce and fixed delays with sync callbacks |
advanceTimersByTimeAsync(ms) | Advances an interval and awaits async timer work | Yes | Still requires a meaningful bound | Retry or delay chains with promises |
runAllTimers() | Drains scheduled timers until empty | Synchronous path | High when callbacks reschedule forever | Finite, known timer graphs |
runAllTimersAsync() | Asynchronously drains until empty | Yes | High for recursive schedules | Finite async timer graphs |
runOnlyPendingTimers() | Runs timers already pending, not newly created during the call | Synchronous path | Lower for one recursive cycle | Poller or interval step |
runOnlyPendingTimersAsync() | Async form of pending-only execution | Yes | Lower for one async recursive cycle | Async poller step |
advanceTimersToNextTimer() | Moves to the next available timer | Synchronous path | Controlled one step at a time | Ordered scheduler assertions |
advanceTimersToNextTimerAsync() | Moves to and awaits the next async timer | Yes | Controlled one step at a time | Async scheduler assertions |
runAllTimers is convenient for a finite group of independent timeouts. It is
dangerous for recursive setTimeout polling or an interval that never clears
because every callback creates or preserves more timer work. Vitest protects
against endless execution with a configurable loop limit documented in the
current API and fake-timer configuration. Do not copy a limit value from
another runner or older release.
runOnlyPendingTimers executes the currently scheduled layer without
continuing to timers created during that execution. That makes one poll cycle
observable. A bounded advanceTimersByTime may still be clearer when the
polling interval itself is the contract.
advanceTimersToNextTimer is useful when timer delays differ and the test cares
about order, not total elapsed milliseconds. Step, assert, then step again.
This keeps failures close to the scheduled transition they protect.
Intervals need explicit stopping behavior. Advancing by three intervals and asserting three calls proves cadence only for that window. The test should also call the production cancellation API, advance again, and prove no fourth call occurs when cancellation is part of the contract. Clearing the timer directly from the test would bypass the public behavior.
For recursive setTimeout, pending-only execution can model one cycle, but the
test should retain the handle or public stop mechanism needed to end the
schedule. Use vi.getTimerCount to confirm cancellation when the count is
stable and meaningful. A pending count alone does not prove callback results,
so pair it with domain assertions.
Timer order can matter when two callbacks share the same due time. Avoid building business behavior around unspecified ordering. If order is required, schedule different boundaries or verify the platform and runner contract that defines it. Fake timers should reveal the application's scheduling assumption, not create an ordering guarantee the real runtime does not provide.
Follow a Numbered Deterministic Timer Workflow
Use the following workflow for each timer-driven behavior:
- State the time contract. Name the pre-boundary state, the trigger instant, and the expected post-boundary state.
- Install fake timers first. Call
vi.useFakeTimersbefore code creates timeouts, intervals, or clock-dependent objects. - Set current time if the contract reads Date. Use
vi.setSystemTimewith an explicit instant and time zone in the input string. - Trigger the production behavior. Call the debounce, retry, poller, expiry check, or scheduler so it creates controlled work.
- Assert the pre-boundary state. Prove nothing fired early and no state changed before the required interval.
- Choose the narrowest advance. Use bounded time, next timer, or pending timers based on the contract. Use an async form for promise-linked work.
- Await the result owned by the test. Timer advancement does not replace awaiting the production promise or async assertion.
- Assert post-boundary behavior and call counts. Verify output, arguments, order, and any required pending work.
- Inspect pending timers when useful. Use
vi.getTimerCountto expose unexpected scheduling rather than silently draining it. - Restore real timers in cleanup. Ensure later tests receive native scheduling even when this test fails.
Run a negative control by changing the production delay or expected boundary. The test should fail on the early or final assertion. A timer test that still passes when the delay is removed did not prove the scheduling contract.
The workflow should also cover cancellation and disposal. After the main assertion, call the production stop, cancel, unmount, or close method. Advance far enough to cross another scheduled boundary and prove no additional work occurs. This catches services that clear one timeout but leave an interval, retry, or secondary timer alive.
For retry logic, assert the attempt count before the delay, after each bounded
advance, and after success or terminal failure. Do not use runAllTimers to
skip directly to the final result if the number and spacing of retries are part
of the contract. Stepwise assertions reveal an immediate retry, an extra
attempt, or a retry that continues after success.
For expiration logic, separate "what time is it?" from "what scheduled work
fires?" A method that checks Date.now() when called may need only
setSystemTime. A service that schedules a timeout until expiry needs timer
advancement. Testing both with one undifferentiated clock jump can obscure
which mechanism is broken.
Add the Pattern to This Repo's Vitest Setup
The repository's vitest.config.ts currently defines aliases, sets
test.environment to node, and includes src/**/*.test.ts. Nothing in that
file enables fake timers globally, so article examples should not claim an
existing policy.
Local ownership is the safest starting point because each test makes clock
replacement visible. If repeated timer tests need the same current time or API
selection, Vitest supports a fakeTimers configuration under test.
import { defineConfig } from 'vitest/config';
import path from 'node:path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'server-only': path.resolve(__dirname, 'src/test/server-only.ts'),
},
},
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
fakeTimers: {
now: new Date('2027-01-01T00:00:00.000Z'),
},
},
});This is an optional extension of the current file, not the repository's
present configuration. Even with defaults configured, tests still call
vi.useFakeTimers when they want the controlled clock. Configuration defines
how that clock is installed.
A global now default improves consistency only when many tests genuinely
share that reference instant. It can also hide tests that should arrange their
own business date. Prefer local setSystemTime for a named calendar boundary,
and use configuration for stable infrastructure defaults such as the APIs to
fake or a neutral clock baseline.
The repository uses a Node environment, so examples should not assume DOM timer behavior or browser animation frames are present. A file that needs a DOM environment should declare or configure it through Vitest's supported environment mechanism. Even then, it is still a simulated test environment, not a real page controlled by a browser automation session.
Teams should keep a small setup-policy test when they customize fake timers.
That test can install the clock, schedule a timeout, advance it, verify Date,
restore real timers, and confirm a later real-timer test behaves normally. It
protects the configuration boundary without forcing every feature test to
repeat configuration details.
The javascript-testing-fake-timers-quiz seed supplies the editorial cases:
debounce at 299 plus 1 milliseconds, recursive scheduling, async advancement,
timer restoration, and frozen system time. Keep the article examples in Vitest
syntax even though the seed discusses both Jest and Vitest. Do not translate
method behavior by name alone; verify each operation in the current vi API.
Frequently Asked Questions About Vitest Fake Timers
How do I enable fake timers in Vitest?
Call vi.useFakeTimers() before the code under test creates timer work.
Advance the controlled clock with the vi operation that matches the
contract, then call vi.useRealTimers() in cleanup so later tests use native
timers.
What is the difference between synchronous and async advancement?
advanceTimersByTime follows the synchronous timer-control path.
advanceTimersByTimeAsync also handles asynchronous timer work documented by
Vitest and returns a promise that must be awaited. Use the async form when timer
callbacks lead into promises or asynchronously scheduled timers.
When should I use runOnlyPendingTimers?
Use it to run the timer layer already scheduled without continuously executing new timers created by those callbacks. It is useful for one recursive polling cycle. Prefer a bounded time advance when elapsed duration is itself part of the behavior.
Does vi.useFakeTimers mock Date?
Current Vitest documentation includes Date among controlled APIs. Use
vi.setSystemTime to pin a known instant after installing fake timers. If the
project configures fakeTimers.toFake, verify that Date remains included.
Why does a later test hang?
The earlier test may have left fake timers active, so later polling or timeout
work no longer follows real time. Restore native timers in afterEach. Also
confirm that required pending work was asserted before restoration rather than
discarded accidentally.
Can Vitest control a real browser page clock?
Not by default. Vitest controls its own test environment. A real browser page has a separate JavaScript context. Use a browser automation clock API such as Playwright Clock for page timers and keep its installation and cleanup within the browser test.
Conclusion: Control Time at One Clear Boundary
Vitest fake timers make scheduling deterministic when the test installs the clock before work is created, advances the narrowest meaningful interval, awaits promise-linked timer work, and restores native APIs after the case. They are most valuable when the contract includes a before and after boundary, not merely eventual execution.
Keep browser clocks, real latency, and external state outside this unit-test boundary. Run a negative control to prove the timer assertion fails when code fires early, late, or more often than expected. For further timing practice, the Playwright clock, async assertion, and timeout scenarios contrast browser-level behavior with the Vitest environment used here.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official vitest.dev reference
vitest.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official vitest.dev reference
vitest.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official vitest.dev reference
vitest.dev
Primary documentation selected and verified for the claims in this guide.
- 04
FAQ / QUICK ANSWERS
Questions testers ask
How do I enable fake timers in Vitest?
Call vi.useFakeTimers before the code under test creates timers. Timers created afterward use the controlled clock until vi.useRealTimers restores native APIs. Install the fake clock in the test or a setup hook, then restore it in cleanup so later tests do not inherit discarded or frozen scheduling state.
What is the difference between advanceTimersByTime and advanceTimersByTimeAsync?
advanceTimersByTime invokes timers scheduled within the requested virtual interval using the synchronous control path. advanceTimersByTimeAsync also waits through asynchronous timer work documented by Vitest, which is necessary when callbacks schedule promises or async timers. Prefer the async variant when the tested chain crosses both timer and promise queues.
When should I use runOnlyPendingTimers?
Use runOnlyPendingTimers when you want to execute timers that were already scheduled without continually draining timers created during those callbacks. It is useful for recursive polling and intervals where runAllTimers would keep finding new work. A bounded time advance can be even clearer when elapsed time is part of the contract.
Does vi.useFakeTimers mock Date?
Current Vitest documentation includes Date among the APIs controlled by fake timers. Use vi.setSystemTime after installing fake timers to test calendar or expiry branches at a known instant. Check fakeTimers.toFake when the project customizes the controlled APIs, because configuration can narrow or extend the default set.
Why does a later test hang after a fake-timer test?
The earlier test may have left fake timers active, so later polling, waits, or timeouts no longer move with real time. Restore native APIs with vi.useRealTimers in failure-safe cleanup. Also finish or intentionally discard pending timer work and avoid relying on a timer scheduled before the fake clock was installed.
Can Vitest fake timers control timers inside a real browser page?
Not automatically. Vitest fake timers control timer APIs in the Vitest test environment. A browser page has its own execution context and clock. Use the browser automation framework's page-clock feature or application-level injection for page timers, then keep that browser lifecycle separate from unit-test timer control.
RELATED GUIDES
Continue the learning route
GUIDE 01
Test Dates and Time Zones Without Flaky JavaScript
Master JavaScript timezone testing with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 02
Debug JavaScript Date and Time Zone Test Failures
A practical guide to debug JavaScript timezone test failures, covering design, implementation, debugging, scale, measurable release gates, and senior interview scenarios.
GUIDE 03
Deterministic Timer and Date Tests with the Playwright Clock API
Control dates, timers, intervals, and inactivity flows with Playwright Clock API examples that keep time-dependent browser tests deterministic.
GUIDE 04
JavaScript Event Loop Ordering in Browser Tests
Master JavaScript event loop browser testing with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 05
Async Orchestration Architecture for JavaScript Test Runners
JavaScript async orchestration architecture: practical design, implementation, debugging, CI, metrics, and interview guidance for QA, SDET, and automation engineers.