Skip to content

Fix: Jest Fake Timers Not Working — setTimeout and setInterval Not Advancing

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

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 called

Or 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 calls

Or 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 hangs

Why 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() — if setTimeout is called before jest.useFakeTimers(), it uses the real timer and won’t be controlled by Jest.
  • Wrong API usagejest.runAllTimers() runs all pending timers synchronously. jest.advanceTimersByTime(ms) advances the clock by ms. Mixing them up or using the wrong one causes the test to not behave as expected.
  • setTimeout inside an async function — if a timer is inside a Promise or async function, the timer is registered asynchronously. Calling runAllTimers() before the timer is registered does nothing.
  • @sinonjs/fake-timers vs 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.
  • clearTimeout or clearInterval called 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.

The interaction between fake timers and Promises is a particularly common source of confusion. Modern fake timers (backed by @sinonjs/fake-timers) fake setTimeout, setInterval, Date, queueMicrotask, and more. But Promise.resolve().then(...) schedules a microtask on the V8 engine’s microtask queue, not through any timer API that Jest replaces. This means advancing fake time doesn’t flush Promises. When your production code mixes await with setTimeout — as in retry logic, debounced fetches, or polling loops — you must flush both the fake timer queue and the microtask queue in the right order.

Another subtle issue: Jest 27 switched the default fake timer implementation from “legacy” to “modern.” Legacy mode only fakes setTimeout, setInterval, clearTimeout, and clearInterval. Modern mode also fakes Date, performance.now(), queueMicrotask, and setImmediate. If you upgraded Jest and your timer tests started failing, the different set of faked globals is likely the cause — especially if your code relies on Date.now() or performance.now() for timing decisions.

Diagnostic Timeline

When your fake timer test fails silently (no error, just wrong assertion), walk through this checklist in order.

Minute 0 — Verify useFakeTimers runs before the timer is registered. Add console.log(setTimeout.toString()) before and after jest.useFakeTimers(). If the output changes from function setTimeout() { [native code] } to a wrapped function, faking is active. If both print native code, useFakeTimers was called too late or never ran.

Minute 1 — Check modern vs legacy mode. Run jest --showConfig | grep fakeTimers to see the project-level config. Then check the test file for an explicit jest.useFakeTimers({ legacyFakeTimers: true }) or jest.useFakeTimers('legacy'). If the config says modern but the test says legacy (or vice versa), behavior differences will surprise you. Modern mode fakes Date and queueMicrotask; legacy does not.

Minute 2 — Test whether Promises flush. Add a minimal check:

jest.useFakeTimers();
let resolved = false;
Promise.resolve().then(() => { resolved = true; });
jest.advanceTimersByTime(0);
console.log('Promise flushed:', resolved);  // false in most modes

If resolved is still false, advancing timers doesn’t flush microtasks. You need await Promise.resolve() or await jest.advanceTimersByTimeAsync(0) (Jest 29.5+) to flush the microtask queue separately.

Minute 3 — Identify async code mixed with timers. Search the code under test for await new Promise(resolve => setTimeout(resolve, ...)). This pattern creates a deadlock: the test awaits the Promise, but the Promise can only resolve when fake time advances, and fake time can only advance after the test continues past the await. The fix is to not await the function — assign it to a variable, advance time, then await the variable.

Minute 4 — Check for captured timer references. Search the module under test for patterns like const delay = setTimeout or const mySetTimeout = global.setTimeout. If the module captures a reference to the real setTimeout at import time (before useFakeTimers replaces it), fake timers never control that module. Move jest.useFakeTimers() to the top of the test file or into jest.config.js with fakeTimers: { enableGlobally: true }.

Minute 5 — Verify no timer is cleared prematurely. Add a spy: jest.spyOn(global, 'clearTimeout'). Run the test. If clearTimeout is called before you advance time, the production code is canceling the timer under certain conditions (a re-render, a state change, a dependency update). Trace the clearTimeout call to understand why.

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();  // PASSES

In 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() in afterEach. 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:

ScenarioMethod to use
Simple one-shot timeoutrunAllTimers() or advanceTimersByTime(delay)
Infinite setIntervalrunOnlyPendingTimers() or advanceTimersByTime(n)
Exact timing assertionsadvanceTimersByTime(ms)
Recursive setTimeoutrunAllTimers() (handles recursively created timers)
Debounce/throttle testingadvanceTimersByTime(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.

Third-party libraries that use their own timers — some libraries (Axios, node-fetch, ws) use internal timer mechanisms or native Node.js timers that Jest’s fake timers don’t intercept. If a library’s timeout feature doesn’t respond to jest.advanceTimersByTime(), check whether the library accepts a custom timer implementation or mock the library’s timeout behavior directly with jest.spyOn.

jest.advanceTimersByTime doesn’t trigger requestAnimationFramerequestAnimationFrame is not a timer. Modern fake timers don’t fake it by default. If your component uses requestAnimationFrame for animations, mock it explicitly:

jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
  cb(Date.now());
  return 0;
});

Recursive setTimeout with runAllTimers causes infinite loop — if your code does setTimeout(() => { /* work */ setTimeout(same, 1000); }, 1000), calling jest.runAllTimers() tries to drain an infinite queue and throws Ran 100000 timers, and there are still more!. Use jest.advanceTimersByTime(specificMs) or jest.runOnlyPendingTimers() instead to advance one step at a time.

For related Jest issues, see Fix: Jest Snapshot Outdated, Fix: Jest ESM Error, Fix: Jest Mock Not Working, and Fix: Jest Async Test Timeout.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles