What this usually means
The root cause is that Jest's fake timers are either not applied at the right time (e.g., after async imports), or there's a boundary where code uses real timers (e.g., native `setTimeout` from a different module, or a Promise microtask that schedules a real timer). Another common scenario is mixing `jest.useFakeTimers('legacy')` with modern `jest.useFakeTimers()` — they have different implementations. Also, `jest.useFakeTimers()` only mocks timers at the time of the call; any timer created before that call remains real.
The first ten minutes — establish facts before touching code.
- 1Run `jest --verbose --no-cache` and look for tests that hang or timeout
- 2Add `console.log(Date.now())` inside the test; if it's real time, fake timers aren't active
- 3Check if `jest.useFakeTimers()` is called before any async imports or `require` statements
- 4Verify you're using the correct fake timer implementation: `jest.useFakeTimers('modern')` or `'legacy'`
- 5Inspect if the code under test uses `process.nextTick` or `setImmediate` — these are not faked by default
The specific files, logs, configs, and dashboards that usually own this bug.
- searchTest file: look for `jest.useFakeTimers()` calls and their position relative to imports
- searchThe module under test: search for `setTimeout`, `setInterval`, `Date.now`, and check if they're used inside closures or promises
- searchJest configuration: `jest.config.js` or `package.json` for `fakeTimers` settings
- searchGlobal setup files: if `jest.useFakeTimers()` is called in a setup file, ensure it runs before tests
- search`node_modules/` — rarely, but check if a library wraps timers (e.g., `sinon`)
Practical causes, not theory. These are the things you will actually find.
- warning`jest.useFakeTimers()` called after some async code has already scheduled real timers
- warningMismatched fake timer implementations: default 'modern' vs 'legacy'
- warningCode uses `new Promise(r => setTimeout(r, 0))` — the promise microtask escapes fake timer scope
- warning`jest.advanceTimersByTime()` not flushing microtasks (use `jest.runAllTimers` or `jest.useFakeTimers({advanceTimers: true})`)
- warning`Date.now()` is mocked separately (e.g., `jest.spyOn(Date, 'now')`) which overrides fake timer's Date mock
- warningTimers created inside `beforeAll` or module scope are not affected by `useFakeTimers` in test
Concrete fix directions. Pick the one that matches your root cause.
- buildMove `jest.useFakeTimers()` to the top of the test file, before any imports
- buildUse `jest.useFakeTimers('modern')` explicitly and call `jest.runAllTimers()` after advancing
- buildFlush pending microtasks with `await Promise.resolve()` before asserting timer effects
- buildReplace `new Promise(r => setTimeout(r, 0))` with `jest.advanceTimersByTime(0)` and await
- buildIf timers are inside async functions, use `jest.useFakeTimers({advanceTimers: true})` to auto-advance
A fix you cannot prove is a guess. Close the loop.
- verifiedAdd a test that asserts `setTimeout` was called: `expect(setTimeout).toHaveBeenCalledTimes(1)`
- verifiedVerify `Date.now()` returns a fixed value: `expect(Date.now()).toBe(1000)` after advancing
- verifiedCheck that a promise resolves after advancing: `await expect(promise).resolves.toBe('done')`
- verifiedRun tests with `--detectOpenHandles` to ensure no lingering timers
- verifiedCompare test duration: a passing test with fake timers should finish in <100ms
Things that make this bug worse or harder to find.
- warningCalling `jest.useFakeTimers()` after `require()` statements in the test file
- warningUsing `jest.useFakeTimers('legacy')` when code uses `Date.now` (legacy does not mock Date)
- warningAssuming `jest.runAllTimers()` will flush microtasks — it won't; use `jest.runAllTimersAsync()` or await
- warningForgetting to call `jest.useRealTimers()` in `afterEach` — causing cross-test contamination
- warningMocking `Date.now` with `jest.spyOn` after fake timers — this breaks the fake timer's Date mock
The Case of the Hanging CI Pipeline
Timeline
- 09:15CI fails with timeout on test 'should update user after 5 seconds'
- 09:20I check the test: uses `jest.useFakeTimers()`, calls `jest.advanceTimersByTime(5000)`, then asserts user update
- 09:25Add `console.log(Date.now())` — it prints a real timestamp, not the fake one
- 09:30I realize `jest.useFakeTimers()` is called inside `beforeEach`, but the module imports happen at the top level
- 09:35The module under test schedules a `setTimeout` in a module-level promise during import
- 09:40I move `jest.useFakeTimers()` to the top of the file, before the `require` statement
- 09:45Test still fails — `Date.now()` still real. I check: the code uses `Date.now` inside a callback
- 09:50I switch to `jest.useFakeTimers('modern')` and call `jest.runAllTimers()` instead of advance
- 09:55Test passes. CI green.
The incident began when a CI pipeline started failing intermittently on a test that verified a 5-second delay before updating a user document. The test used `jest.useFakeTimers()` and `jest.advanceTimersByTime(5000)`, but the assertion never passed. I initially suspected a race condition with the database mock, but adding a simple `Date.now()` log revealed the timers were still real.
I traced the issue to the module under test: it imported a configuration file that initialized a `setTimeout` at module load time. Since `jest.useFakeTimers()` was called in `beforeEach`, after the module was already loaded, that timer was real and never advanced. Moving `jest.useFakeTimers()` to the top of the test file, before any imports, fixed the initial problem.
But the test still failed. The code used `Date.now()` inside the setTimeout callback, and the legacy fake timer implementation does not mock `Date.now()`. Switching to `jest.useFakeTimers('modern')` and replacing `advanceTimersByTime` with `runAllTimers` resolved the issue. The lesson: always use 'modern' unless you have a specific reason not to, and always fake timers before any module code runs.
Root cause
`jest.useFakeTimers()` called after module imports that already scheduled real timers, combined with using the legacy implementation that doesn't mock `Date.now()`.
The fix
Move `jest.useFakeTimers()` before all imports and switch to `jest.useFakeTimers('modern')`.
The lesson
Fake timers must be set up before any code that creates timers runs, and always prefer the modern implementation for full Date and timer mocking.
Jest ships two fake timer implementations: 'legacy' and 'modern' (default since Jest 27). The legacy implementation mocks `setTimeout`, `setInterval`, `clearTimeout`, etc., but does NOT mock `Date.now()` or `process.hrtime`. The modern implementation replaces the entire timer API using `@sinonjs/fake-timers`, including `Date`, `performance.now`, and `queueMicrotask`.
If your code relies on `Date.now()` for timeouts (e.g., `setTimeout(() => {}, Date.now() - someTime)`), the legacy mock will fail silently. Always explicitly set `jest.useFakeTimers('modern')` unless you have a reason to use legacy. You can also configure globally in `jest.config.js` with `fakeTimers: { enableGlobally: true }`.
A frequent footgun is writing `jest.useFakeTimers()` inside a `beforeEach` or `describe` block. If the module under test has top-level code that creates timers (e.g., a recurring health check), those timers are instantiated when the module is `require`'d, which happens before the test runs. Those timers are real and never advanced.
The fix is to place `jest.useFakeTimers()` at the top of the test file, before any `require` or `import` statements. Alternatively, you can use `jest.resetModules()` and re-import the module after faking timers: `jest.useFakeTimers(); const mod = require('./module');`. This ensures the module's timer-creating code runs under fake timers.
A common pattern that breaks fake timers: `new Promise(resolve => setTimeout(resolve, 1000))`. Here, the promise executor runs synchronously, scheduling a real timer (if fake timers are not active). Even if fake timers are active, calling `jest.advanceTimersByTime(1000)` does NOT flush the promise microtask that resolves after the timer fires. The promise's `.then()` will not execute until you manually flush microtasks.
The solution is to `await Promise.resolve()` after advancing timers, or use `jest.runAllTimersAsync()` (available in Jest 27+) which returns a promise that resolves after flushing both timers and microtasks. Alternatively, you can configure fake timers with `advanceTimers: true` to auto-advance on each timer call.
If you don't restore real timers in `afterEach`, later tests may inherit fake timers, causing unexpected behavior. Always call `jest.useRealTimers()` in `afterEach`, or use `jest.useFakeTimers({ legacyFakeTimers: false })` with a `clearMocks` configuration. Additionally, if you spy on `Date.now` after faking timers, you overwrite the fake timer's Date mock. Avoid mixing `jest.spyOn(Date, 'now')` with `jest.useFakeTimers('modern')`.
A good practice is to have a single `beforeEach` that sets up fake timers and an `afterEach` that tears them down. Use `jest.useFakeTimers('modern')` consistently. If you need to mock `Date` differently, do it before faking timers.
Frequently asked questions
Why does `jest.runAllTimers()` throw 'Ran 100000 timers or more'?
This error means you have an infinite loop of timers: each timer callback schedules another timer. Check for `setInterval` or recursive `setTimeout` that never stops. Use `jest.advanceTimersByTime()` with a finite time, or break the loop by clearing timers after a certain number of calls.
How do I fake timers for a specific test only?
Call `jest.useFakeTimers()` inside that test's `it` block, and call `jest.useRealTimers()` at the end of the test or in `afterEach`. However, be careful: if the module was already loaded with real timers, you'll need to `jest.resetModules()` and re-import. A cleaner approach is to use `jest.isolateModules()` to import the module fresh within the test.
Does `jest.useFakeTimers('modern')` mock `setImmediate` and `process.nextTick`?
Yes, the modern implementation mocks `setImmediate` and `process.nextTick` as timer-like APIs. However, `queueMicrotask` is not directly mocked; it's handled as a microtask. If your code uses `process.nextTick`, you can advance it by calling `jest.advanceTimersByTime(0)` after the tick is scheduled.
Why does my async test hang even with fake timers?
If your test uses `async/await` and the awaited promise depends on a timer, you must advance timers AND flush microtasks. Use `jest.runAllTimersAsync()` (returns a promise) or `await jest.advanceTimersByTimeAsync(ms)`. Also ensure that the promise is created after faking timers.
Can I use fake timers with `jest.useFakeTimers({advanceTimers: true})`?
Yes, this option automatically advances fake timers by the elapsed time when `setTimeout` or `setInterval` is called. It's useful for code that uses timers but you don't want to manually step through them. However, it can make tests harder to reason about because time advances implicitly. Use it sparingly.