Community JavaScript Snippet
Freeze Time and Fast-Forward in Jest
The clock-control playbook I keep on a sticky note: freeze `Date.now`, advance pending timers without sleeping, and write tests for debounce/throttle helpers that actually finish in milliseconds.
Freeze Time and Fast-Forward in Jest
The clock-control playbook I keep on a sticky note: freeze `Date.now`, advance pending timers without sleeping, and write tests for debounce/throttle helpers that actually finish in milliseconds.
By @rohanbakr
May 2, 2026
·
Updated May 20, 2026
193 views
2
4.6 (8)
The fundamental move is dependency injection: the unit under test receives a clock object instead of reaching for the global Date.now and setTimeout. In production you wire it to globalThis; in tests you wire it to a fake whose advance method walks the queue deterministically. This is exactly what jest.useFakeTimers() does under the hood, but writing a 30-line clock once teaches you why the flaky tests happen: any code that reads time without going through the injected clock will desync from the test. The test for our debounce now finishes in microseconds and never relies on setTimeout(0) tricks.
This is what jest.useFakeTimers('modern') actually does: it monkey-patches Date.now, setTimeout, setInterval, and a handful of other timer globals with a fake whose internal queue your test drives. Reproducing the technique in plain JS makes the failure modes obvious: anything that captured a reference to the real setTimeout before you patched (a vendored library, a closure formed at import time) is invisible to your fake clock and will flake. In real Jest you fix that with jest.isolateModules or by importing the unit-under-test after the fake-timers call. Always restore originals at the end so other tests do not run against a frozen clock.
The thing nobody tells you about jest.advanceTimersByTime is that it does not flush the microtask queue. If your callback does something().then(next), the timer fires and queues a microtask, but the next continuation only runs once the JS event loop pumps. In real Jest you sprinkle await Promise.resolve() between advances, or use jest.advanceTimersByTimeAsync (which yields internally). The pattern in the example, advance plus two yields, is the same recipe applied to the fake clock above. Once you see this you stop being surprised that attempts is one behind what you expected.
