Jest Timer Strategy: When to Use Modern Fake Timers vs Real Timers
Decide when to mock timers in Jest. Modern fake timers keep tests fast and deterministic, but real timers are needed for integration tests that depend on actual elapsed time. A quick decision table, trade‑offs, and a concrete debounce test guide the choice.
21 Aug 2026, 10:52 UTC

Problem: Testing Code That Depends on Time
Functions that use setTimeout, setInterval, Date.now(), or performance.now() are common in debouncing, retry logic, cache TTLs, and scheduled jobs. When such code is exercised in a test, you face two basic choices:
- Mock the timer API to advance time instantly.
- Leave timers real and let the test wait for actual elapsed time.
Choosing the wrong strategy can make tests slow, flaky, or hide race conditions. This guide helps you pick the right approach for Jest‑based projects.
Decision: Use Modern Fake Timers for Most Unit Tests, Real Timers for Integration
For unit‑level tests that exercise logic scheduled by timers, use jest.useFakeTimers('modern') (the default since Jest 27). This mocks setTimeout, setInterval, Date, performance.now(), and queueMicrotask, giving deterministic, instant time progression.
Reserve jest.useRealTimers() for integration or end‑to‑end tests where the *actual* passage of time matters (e.g., verifying a 5‑minute cache expiration against a real clock).
Options Compared in a Compact Table
| Timer Mode | What is Mocked? | Speed Impact | Determinism | Common Use‑Case |
|---|---|---|---|---|
| Modern Fake (default Jest 27+) | setTimeout, setInterval, Date, performance.now(), queueMicrotask | Fast – no real wait | High – timer callbacks run immediately when advanced | Unit tests for debounce, retry, cache TTL |
| Legacy Fake | Only setTimeout and setInterval | Fast – but Date.now() still real | Partial – Date.now() may differ from mocked timers | Legacy projects that rely on older Jest behavior |
| Real Timers | All native timing APIs | Slow – test waits for real time | True – reflects actual OS timers | Integration tests where elapsed time matters |
Trade‑Offs Explained
- Speed vs Accuracy: Fake timers remove waiting, but risk missing interleaving of promises and timers. Real timers are accurate but slow, especially for long backoff sequences.
- Determinism vs Race Conditions: In modern fake timers,
queueMicrotaskis mocked, so microtasks run predictably afterjest.advanceTimersByTime(). However, if your code mixesasync/awaitwith timers, you may still need toawait Promise.resolve()to flush microtasks. - Configuration Complexity: Global configuration (e.g.,
timers: 'modern'in jest.config.js) applies to all suites, which can bleed into tests that expect real timers. Localjest.useFakeTimers()inbeforeEachgives fine‑grained control. - Legacy vs Modern: The legacy implementation does not mock
Date, so tests that assert onDate.now()after timer advancement will fail unless you also mock the clock withjest.setSystemTime().
Concrete Implementation: Debounce Function Test
Below is a minimal debounce implementation and a Jest test that demonstrates the use of modern fake timers.
// debounce.js
export function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// debounce.test.js
import { debounce } from './debounce';
describe('debounce', () => {
beforeEach(() => {
jest.useFakeTimers('modern');
});
afterEach(() => {
jest.useRealTimers();
});
it('calls the function only once after rapid invocations', () => {
const fn = jest.fn();
const debounced = debounce(fn, 100);
debounced();
debounced();
debounced();
// No calls yet because the timer hasn't advanced
expect(fn).not.toBeCalled();
// Fast‑forward 100ms – the callback should run once
jest.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledTimes(1);
// Advance past any remaining timers (none expected)
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1);
});
});
Key points in the test:
jest.useFakeTimers('modern')mocksDateand microtasks.- After invoking the debounced function three times, the callback is not yet called.
- Advancing timers by the delay triggers the callback exactly once.
- Running all timers afterward confirms no hidden timers remain.
Validating the Mocked Clock
To confirm that Date.now() is mocked, you can log its value before and after setting a system time:
beforeEach(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date('2026-01-01T00:00:00Z'));
});
it('shows mocked Date.now()', () => {
console.log(Date.now()); // 1672531200000
});
Running the suite will output the mocked timestamp. If you run the same test with jest.useRealTimers(), the output will differ, confirming the switch.
Scope Control and Leaking Timers
Because timer mocks are global, it is safest to enable them in beforeEach and restore in afterEach. If you prefer per‑test opt‑in, Jest allows jest.useFakeTimers() inside a single test block, but remember to call jest.useRealTimers() at the end of the test to avoid leaking the mocked clock into subsequent tests.
Configuration Alternatives
Instead of per‑test calls, you can set a global default in jest.config.js:
module.exports = {
timers: 'modern', // or 'legacy'
};
This applies to all test files, so use it only if your entire suite is compatible with that timer mode. For mixed suites, per‑test control remains clearer.
Common Pitfalls and How to Avoid Them
- Recursive setTimeout Loops:
jest.runAllTimers()will loop forever if a timer reschedules itself. Usejest.advanceTimersByTime()with a known duration instead. - Promises with Fake Timers: If a promise resolves after a timer, you may need
await Promise.resolve()orawait flushPromises()to allow microtasks to run after advancing timers. - Network I/O with Fake Timers: Code that awaits real network requests can hang if timers are mocked and the request logic uses timers internally. In such cases, either mock the I/O or switch to real timers for that test.
- Version Mismatch: Jest 27 changed the default timer implementation to modern. Verify your
jest --versionbefore assumingDateis mocked.
Practical Checklist Before Running Tests
- Identify if the test logic depends on actual elapsed time (integration) or scheduled callbacks (unit).
- For unit logic, call
jest.useFakeTimers('modern')inbeforeEachandjest.useRealTimers()inafterEach. - Use
jest.advanceTimersByTime(ms)to simulate passage of time; combine withawait Promise.resolve()if microtasks are involved. - Run the suite with
--detectOpenHandlesto catch any timers left pending after teardown. - If tests hang or fail unexpectedly, consider switching to legacy fake timers or real timers to surface hidden race conditions.
Conclusion
Modern fake timers are the default and most powerful choice for unit tests in Jest. They keep suites fast, deterministic, and easy to reason about. Real timers should be reserved for integration scenarios where the real passage of time is part of the behavior under test. By controlling timer mode per test and validating the mocked clock, you can avoid flaky tests and hidden bugs in time‑dependent code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.