Asynchronous execution in JavaScript is governed by the single-threaded Event Loop. Precise understanding of execution queues is essential for architecting non-flaky automated tests:
Promise.then, catch, finally), queueMicrotask(), and process.nextTick(). The microtask queue is exhaustively drained before the runtime yields to any macrotask or browser rendering pass.setTimeout, setInterval, setImmediate), network I/O events, and UI dispatchers.Waiting for real physical time during tests degrades CI velocity (e.g., waiting 5 seconds to test session expiration would grind test suites to a halt). Vitest eliminates this overhead via Fake Timers.
+-------------------------------------------------------------+
| JavaScript Event Loop |
+-------------------------------------------------------------+
|
+----------------------------+----------------------------+
| |
v v
+-------------------------------+ +-------------------------------+
| Microtask Queue | | Macrotask Queue |
| - Promise.then / await | | - setTimeout / setInterval |
| - queueMicrotask | | - setImmediate / I/O |
| (Drained exhaustively | | (Intercepted by Vitest |
| before any macrotask) | | Fake Timers Engine) |
+-------------------------------+ +-------------------------------+
|
v
+-------------------------------+
| vi.advanceTimersByTime(ms) |
| Instant virtual clock jump |
| without physical delay |
+-------------------------------+AbortError / TimeoutError handling via AbortController and AbortSignal.timeout().vi.setSystemTime() to test JWT expirations, subscription renewals, or dynamic interest calculations deterministically.setInterval handles retaining component instances in worker memory post-unmount.vi.advanceTimersByTimeAsync#Consider a strictly typed debounce implementation. Because the wrapped callback can return a promise or initiate async actions, we use advanceTimersByTimeAsync.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
export function debounce<TArgs extends readonly unknown[]>(
fn: (...args: TArgs) => void | Promise<void>,
delayMs: number
): (...args: TArgs) => void {
let timerId: ReturnType<typeof setTimeout> | null = null;
return (...args: TArgs) => {
if (timerId !== null) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
timerId = null;
void fn(...args);
}, delayMs);
};
}
describe('debounce with virtual timers', () => {
beforeEach(() => {
// Intercept native timers prior to test execution
vi.useFakeTimers();
});
afterEach(() => {
// Mandatorily restore native runtime clocks
vi.useRealTimers();
});
it('invokes target handler exactly once after the delay expires', async () => {
const targetFn = vi.fn();
const debouncedFn = debounce(targetFn, 300);
// Rapid successive invocations
debouncedFn('alpha');
debouncedFn('beta');
debouncedFn('gamma');
// At 299ms, the timer has not elapsed
await vi.advanceTimersByTimeAsync(299);
expect(targetFn).not.toHaveBeenCalled();
// Advance 1ms further (300ms total)
await vi.advanceTimersByTimeAsync(1);
expect(targetFn).toHaveBeenCalledTimes(1);
expect(targetFn).toHaveBeenCalledWith('gamma');
});
it('resets timer when called again before delay expiration', async () => {
const targetFn = vi.fn();
const debouncedFn = debounce(targetFn, 500);
debouncedFn('initial');
await vi.advanceTimersByTimeAsync(300); // 300ms elapsed
// Re-triggering resets the delay window
debouncedFn('updated');
await vi.advanceTimersByTimeAsync(300); // 300ms elapsed since second call (600ms total)
expect(targetFn).not.toHaveBeenCalled();
// Complete remaining 200ms
await vi.advanceTimersByTimeAsync(200);
expect(targetFn).toHaveBeenCalledTimes(1);
expect(targetFn).toHaveBeenCalledWith('updated');
});
});AbortController#Modern web architectures employ AbortController to cancel hanging operations upon view transitions or network timeouts.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
export async function fetchWithTimeout<T>(
url: string,
timeoutMs: number,
clientFetch: typeof fetch = fetch
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort(new DOMException('Request timed out', 'TimeoutError'));
}, timeoutMs);
try {
const response = await clientFetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return (await response.json()) as T;
} finally {
// Prevent uncleaned timer leaks in the event loop
clearTimeout(timeoutId);
}
}
describe('fetchWithTimeout', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('aborts unresponsive network requests upon timeout expiration', async () => {
// Network mock that hangs indefinitely until aborted
const mockFetch = vi.fn((_url: string, init?: RequestInit) => {
return new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => {
reject(init.signal?.reason);
});
});
});
const fetchPromise = fetchWithTimeout('https://api.example.com/data', 2000, mockFetch as typeof fetch);
// Fast-forward virtual clock 2000ms
await vi.advanceTimersByTimeAsync(2000);
await expect(fetchPromise).rejects.toThrow('Request timed out');
});
it('returns valid data and disarms timer when response arrives in time', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' }),
} as Response);
const result = await fetchWithTimeout<{ status: string }>(
'https://api.example.com/data',
5000,
mockFetch as typeof fetch
);
expect(result).toEqual({ status: 'success' });
// Verify no lingering timer handles exist in the queue
expect(vi.getTimerCount()).toBe(0);
});
});vi.setSystemTime()#Deterministically testing subscriptions, licenses, and invoice generation requires pinning Date.now() to a known epoch.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
export interface Subscription {
expiresAt: string; // ISO 8601
status: 'active' | 'cancelled';
}
export function isSubscriptionValid(sub: Subscription): boolean {
if (sub.status !== 'active') {
return false;
}
const now = new Date();
const expiry = new Date(sub.expiresAt);
return expiry.getTime() > now.getTime();
}
describe('isSubscriptionValid with system clock pinning', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('validates subscription before exact expiration threshold', () => {
// Freeze system time: June 1, 2026, 12:00:00 UTC
vi.setSystemTime(new Date('2026-06-01T12:00:00.000Z'));
const sub: Subscription = {
status: 'active',
expiresAt: '2026-06-01T12:00:01.000Z', // Expires in 1 second
};
expect(isSubscriptionValid(sub)).toBe(true);
// Advance virtual time by 2 seconds
vi.advanceTimersByTime(2000);
expect(isSubscriptionValid(sub)).toBe(false);
});
});When application code schedules an infinite interval or recursive setTimeout (such as background polling), calling vi.runAllTimers() creates an infinite loop, freezing the worker thread until a hard timeout occurs.
// ❌ Anti-pattern: infinite scheduler loop
setInterval(pollServer, 1000);
vi.runAllTimers(); // Test deadlocks: interval repeatedly reschedules itself!
// ✅ Correct pattern: invoke vi.runOnlyPendingTimers() or discrete advances
vi.runOnlyPendingTimers(); // Executes only timers already queued at invocation time
// or
await vi.advanceTimersByTimeAsync(1000); // Progress discrete intervalsLeaving virtual clocks active leaks modified globals across subsequent test files. Any tests expecting genuine microtask resolution or real network timeouts will hang indefinitely.
// ❌ Anti-pattern: leaked fake timer configuration
it('tests timer logic', () => {
vi.useFakeTimers();
// ... missing teardown
});
// ✅ Correct pattern: mandatory teardown in afterEach
afterEach(() => {
vi.clearAllTimers();
vi.useRealTimers();
});When a timer triggers an async/await function or promise reaction, synchronous vi.advanceTimersByTime finishes before the microtask queue evaluates, causing assertions to fail prematurely.
// ❌ Anti-pattern: promise microtask inside timer has not yet run
setTimeout(async () => {
await fetchUserProfile();
state.isLoaded = true;
}, 500);
vi.advanceTimersByTime(500);
expect(state.isLoaded).toBe(true); // FAILS! Microtask is still queued
// ✅ Correct pattern: advance asynchronously to drain microtasks
await vi.advanceTimersByTimeAsync(500);
expect(state.isLoaded).toBe(true); // PASSES!Dangling intervals inside React useEffect hooks retain closures in memory, degrading test runner throughput and triggering flaky suite crashes.
// ✅ Verifying unmount cleanup via timer inspection
it('disarms interval upon unmount', () => {
const { unmount } = render(<AutoRefreshWidget />);
expect(vi.getTimerCount()).toBe(1);
unmount();
expect(vi.getTimerCount()).toBe(0); // Asserts cleanup function cleared the timer
});Call vi.useFakeTimers() specifying custom toFake configurations if selective mocking is required.
Execute the application operation that schedules asynchronous work (click, debounce dispatch, polling initiation).
Step virtual time forward via await vi.advanceTimersByTimeAsync(ms), giving queued microtasks opportunity to drain.
Verify state invariants, assert timer disarmament with vi.getTimerCount(), and call vi.useRealTimers().