Fix: Jest Fake Timers Not Working — setTimeout and setInterval Not Advancing
Quick Answer
How to fix Jest fake timers not working — useFakeTimers setup, runAllTimers vs advanceTimersByTime, async timers, React testing with act(), and common timer test mistakes.
The Problem
Jest fake timers are set up but timers don’t advance:
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 1000);
jest.runAllTimers();
expect(callback).toHaveBeenCalled(); // FAILS — callback was never calledOr advanceTimersByTime doesn’t trigger the callback:
jest.useFakeTimers();
const callback = jest.fn();
setInterval(callback, 500);
jest.advanceTimersByTime(2000); // Should trigger callback 4 times
expect(callback).toHaveBeenCalledTimes(4); // FAILS — 0 callsOr fake timers work in isolation but break when async code is involved:
jest.useFakeTimers();
async function fetchWithRetry() {
await new Promise(resolve => setTimeout(resolve, 1000));
return fetch('/api/data');
}
// Timer doesn't advance because the async function is awaiting it
await fetchWithRetry(); // Test hangsWhy This Happens
Jest’s fake timer system replaces the global timer functions (setTimeout, setInterval, Date, etc.) with mock versions that only advance when explicitly told to. Common causes of failure:
- Timer registered before
useFakeTimers()— ifsetTimeoutis called beforejest.useFakeTimers(), it uses the real timer and won’t be controlled by Jest. - Wrong API usage —
jest.runAllTimers()runs all pending timers synchronously.jest.advanceTimersByTime(ms)advances the clock byms. Mixing them up or using the wrong one causes the test to not behave as expected. setTimeoutinside an async function — if a timer is inside a Promise or async function, the timer is registered asynchronously. CallingrunAllTimers()before the timer is registered does nothing.@sinonjs/fake-timersvs legacy mode — Jest has two fake timer implementations. The modern one (default in Jest 27+) may behave differently from the legacy one for edge cases.clearTimeoutorclearIntervalcalled unintentionally — if production code clears a timer under certain conditions, fake timers won’t fire it.- Module-level timers — if a module registers a timer at import time (before
useFakeTimers()), that timer uses real timers.
Fix 1: Call useFakeTimers Before the Timer Is Registered
// WRONG — timer registered with real setTimeout before faking
const callback = jest.fn();
setTimeout(callback, 1000); // Uses real setTimeout
jest.useFakeTimers(); // Fakes timers AFTER registration — too late
jest.runAllTimers();
expect(callback).toHaveBeenCalled(); // FAILS// CORRECT — fake timers before any timer registration
jest.useFakeTimers(); // Must come first
const callback = jest.fn();
setTimeout(callback, 1000); // Now uses Jest's fake setTimeout
jest.runAllTimers();
expect(callback).toHaveBeenCalled(); // PASSESIn beforeEach — standard setup:
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers(); // Restore real timers after each test
});
test('timer fires after 1 second', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
jest.advanceTimersByTime(999);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1);
expect(callback).toHaveBeenCalledTimes(1);
});Common Mistake: Not calling
jest.useRealTimers()inafterEach. Fake timers bleed into subsequent tests, causing intermittent failures in other test files.
Fix 2: Choose the Right Timer Advancement Method
Jest provides several methods to advance fake timers — each serves a different purpose:
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 1000);
setInterval(callback, 500);
// runAllTimers — runs ALL pending timers until the queue is empty
// Use for: simple synchronous timers with known end state
jest.runAllTimers();
// Runs: 500ms interval, 1000ms timeout, then continues intervals until empty
// ⚠️ Infinite setInterval loops will cause runAllTimers to throw
// runOnlyPendingTimers — runs timers that are pending RIGHT NOW (not future ones)
// Use for: one step at a time, or when intervals would loop forever
jest.runOnlyPendingTimers();
// Runs: 500ms interval fires once (it was pending), 1000ms timeout is not yet due
// advanceTimersByTime(ms) — moves clock forward by ms, firing all timers in that range
// Use for: testing time-dependent behavior precisely
jest.advanceTimersByTime(600);
// Fires: 500ms interval (1 time), does NOT fire the 1000ms timeout yet
jest.advanceTimersByTime(500);
// Fires: 500ms interval again (now at 1100ms total), 1000ms timeout (fired at 1000ms)Choosing the right method:
| Scenario | Method to use |
|---|---|
| Simple one-shot timeout | runAllTimers() or advanceTimersByTime(delay) |
Infinite setInterval | runOnlyPendingTimers() or advanceTimersByTime(n) |
| Exact timing assertions | advanceTimersByTime(ms) |
Recursive setTimeout | runAllTimers() (handles recursively created timers) |
| Debounce/throttle testing | advanceTimersByTime(debounceDelay) |
Fix 3: Handle Async Timers with act() in React
In React tests with @testing-library/react, timers inside components need act() to process state updates:
// Component with a timer
function DelayedMessage() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setVisible(true), 1000);
return () => clearTimeout(timer);
}, []);
return visible ? <p>Hello!</p> : null;
}
// Test
import { render, screen } from '@testing-library/react';
import { act } from 'react';
test('shows message after delay', async () => {
jest.useFakeTimers();
render(<DelayedMessage />);
expect(screen.queryByText('Hello!')).not.toBeInTheDocument();
// Wrap timer advancement in act() so React processes state updates
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.getByText('Hello!')).toBeInTheDocument();
jest.useRealTimers();
});For async state updates:
test('shows message after delay', async () => {
jest.useFakeTimers();
render(<DelayedMessage />);
await act(async () => {
jest.advanceTimersByTime(1000);
// Awaiting act() flushes microtasks (Promise resolutions) after timer fires
});
expect(screen.getByText('Hello!')).toBeInTheDocument();
});Fix 4: Test Async Functions with Fake Timers
When a timer is inside an async function, you need to advance the timer while the async function is waiting:
// Function under test
async function delayedOperation() {
await new Promise(resolve => setTimeout(resolve, 2000));
return 'done';
}
// WRONG — awaiting the function blocks until the timer fires,
// but fake timers need to be advanced manually
test('wrong approach', async () => {
jest.useFakeTimers();
const result = await delayedOperation(); // ← Hangs forever — timer never advances
expect(result).toBe('done');
});// CORRECT — start the async function, then advance timers, then await
test('correct approach', async () => {
jest.useFakeTimers();
// Start the async operation without awaiting it
const promise = delayedOperation();
// Advance fake time — this resolves the internal setTimeout
jest.advanceTimersByTime(2000);
// Now await the result — the timer already fired
const result = await promise;
expect(result).toBe('done');
});With jest.runAllTimersAsync() (Jest 29.1+):
test('using runAllTimersAsync', async () => {
jest.useFakeTimers();
const promise = delayedOperation();
// Runs all timers AND flushes microtasks — handles async timer chains
await jest.runAllTimersAsync();
const result = await promise;
expect(result).toBe('done');
});Fix 5: Test Debounce and Throttle Functions
Fake timers are ideal for testing debounce/throttle without waiting real milliseconds:
import { debounce } from 'lodash';
test('debounce only calls function after wait period', () => {
jest.useFakeTimers();
const callback = jest.fn();
const debounced = debounce(callback, 300);
// Call debounced function multiple times quickly
debounced();
debounced();
debounced();
// Before wait period — callback not called yet
expect(callback).not.toHaveBeenCalled();
// Advance past debounce delay
jest.advanceTimersByTime(300);
// Now callback fired exactly once (debounced)
expect(callback).toHaveBeenCalledTimes(1);
});
test('throttle calls function at most once per interval', () => {
jest.useFakeTimers();
const callback = jest.fn();
const throttled = throttle(callback, 500);
throttled(); // Fires immediately (first call)
throttled(); // Throttled — within interval
throttled(); // Throttled — within interval
expect(callback).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(500);
throttled(); // Fires again (new interval)
expect(callback).toHaveBeenCalledTimes(2);
});Fix 6: Fake the Date Object
jest.useFakeTimers() in modern mode also fakes Date:
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-01-01T00:00:00Z'));
// Date.now() returns the fake time
console.log(new Date().toISOString()); // '2026-01-01T00:00:00.000Z'
console.log(Date.now()); // 1767225600000
// Advance time
jest.advanceTimersByTime(60 * 60 * 1000); // 1 hour
console.log(new Date().toISOString()); // '2026-01-01T01:00:00.000Z'Test time-dependent logic:
function isWeekend() {
const day = new Date().getDay();
return day === 0 || day === 6; // Sunday = 0, Saturday = 6
}
test('isWeekend returns true on Saturday', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-03-21T12:00:00Z')); // Saturday
expect(isWeekend()).toBe(true);
});
test('isWeekend returns false on Monday', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-03-23T12:00:00Z')); // Monday
expect(isWeekend()).toBe(false);
});Fix 7: Configure Legacy vs Modern Fake Timers
Jest has two implementations. If modern fake timers cause issues, try legacy mode:
// Modern (default in Jest 27+) — fakes setTimeout, setInterval, Date, queueMicrotask, etc.
jest.useFakeTimers();
// or explicitly:
jest.useFakeTimers({ legacyFakeTimers: false });
// Legacy (only fakes setTimeout, setInterval, clearTimeout, clearInterval, Date)
jest.useFakeTimers({ legacyFakeTimers: true });
// Shorter alias:
jest.useFakeTimers('legacy');Configure globally in jest.config.js:
// jest.config.js
module.exports = {
fakeTimers: {
enableGlobally: true, // Apply fake timers to all tests automatically
legacyFakeTimers: false,
// Specific globals to fake (omitting Date keeps it real)
doNotFake: ['Date'],
},
};Modern fake timers additional options:
jest.useFakeTimers({
now: new Date('2026-01-01'), // Initial fake date
advanceTimeDelta: 20, // How much fake time advances per real ms (for real timers in fake mode)
doNotFake: [
'nextTick', // Don't fake process.nextTick
'Date', // Don't fake Date
],
});Still Not Working?
Verify the timer function is from the global scope — if the code under test captures setTimeout in a closure before useFakeTimers() is called, it holds a reference to the real timer:
// module.js — captures real setTimeout at import time
const delay = setTimeout; // Captured before Jest replaces it
export function runAfterDelay(fn) {
delay(fn, 1000); // Uses the real setTimeout — fake timers won't work
}Fix: don’t capture timer globals. Call setTimeout directly.
Check for --fakeTimers flag conflicts — if jest.config.js sets fakeTimers: { enableGlobally: true } and a test also calls jest.useFakeTimers(), they may conflict. Use one approach consistently.
setImmediate and process.nextTick — modern fake timers replace setImmediate but not process.nextTick by default. If your code uses process.nextTick, advance it with jest.runAllImmediates() or use await Promise.resolve() to flush the microtask queue.
For related Jest issues, see Fix: Jest Snapshot Outdated and Fix: Jest ESM Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Jest Setup File Not Working — setupFilesAfterFramework Not Running or Globals Not Applied
How to fix Jest setup file issues — setupFilesAfterFramework vs setupFiles, global mocks not applying, @testing-library/jest-dom matchers, module mocking in setup, and TypeScript setup files.
Fix: Jest Async Test Timeout — Exceeded 5000ms or Test Never Resolves
How to fix Jest async test timeouts — missing await, unresolved Promises, done callback misuse, global timeout configuration, fake timers, and async setup/teardown issues.
Fix: Jest Coverage Not Collected — Files Missing from Coverage Report
How to fix Jest coverage not collecting all files — collectCoverageFrom config, coverage thresholds, Istanbul ignore comments, ts-jest setup, and Babel transform issues.
Fix: Jest Module Mock Not Working — jest.mock() Has No Effect
How to fix Jest module mocks not working — hoisting behavior, ES module mocks, factory functions, mockReturnValue vs implementation, and clearing mocks between tests.